mesa_test 0.2.12 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mesa_test +262 -432
  3. data/lib/mesa_test.rb +438 -1158
  4. metadata +7 -8
@@ -11,9 +11,10 @@ require 'json'
11
11
  MesaDirError = Class.new(StandardError)
12
12
  TestCaseDirError = Class.new(StandardError)
13
13
  InvalidDataType = Class.new(StandardError)
14
+ GitHubError = Class.new(StandardError)
14
15
 
15
- Commit = Struct.new(:revision, :author, :datetime, :message)
16
- DEFAULT_REVISION = 10_000
16
+ GITHUB_HTTPS = 'https://github.com/MESAHub/mesa.git'.freeze
17
+ GITHUB_SSH = 'git@github.com:MESAHub/mesa.git'.freeze
17
18
 
18
19
  class MesaTestSubmitter
19
20
  DEFAULT_URI = 'https://mesa-test-hub.herokuapp.com'.freeze
@@ -28,7 +29,8 @@ shown in parentheses at the end of a prompt. Pressing enter will accept the
28
29
  default values.
29
30
 
30
31
  To submit to MESATestHub, a valid computer name, email address, and password
31
- are all required. All other data are useful, but optional. Any data
32
+ are all required. To actually run a test, you need to specify a location for
33
+ your base MESA git repository. All other data are useful, but optional. Any data
32
34
  transferred to MESATestHub will be encrypted via HTTPS, but be warned that your
33
35
  e-mail and password will be stored in plain text.'
34
36
  # Get computer name
@@ -36,11 +38,6 @@ e-mail and password will be stored in plain text.'
36
38
  "(#{s.computer_name}):", :blue)
37
39
  s.computer_name = response unless response.empty?
38
40
 
39
- # Get user name
40
- response = shell.ask 'What is the name of the operator of this ' \
41
- "computer? (#{s.user_name}):", :blue
42
- s.user_name = response unless response.empty?
43
-
44
41
  # Get user e-mail
45
42
  response = shell.ask 'What is the email you can be reached ' \
46
43
  "at (required)? (#{s.email}):", :blue
@@ -51,40 +48,33 @@ e-mail and password will be stored in plain text.'
51
48
  "#{s.email} (required)? (#{s.password})", :blue
52
49
  s.password = response unless response.empty?
53
50
 
51
+ # Determine if we'll use ssh or https to access github
52
+ response = shell.ask 'When accessing GitHub, which protocol do you '\
53
+ 'want to use? ', :blue, limited_to: %w[ssh https]
54
+ s.github_protocol = response.strip.downcase.to_sym
55
+
56
+ # Get location of source MESA repo (the mirror)
57
+ response = shell.ask "Where is/should your mirrored MESA repository " \
58
+ "located? This is where a mirror will be stored from which test " \
59
+ "repos will be generated. You won't touch this in regular operation. " \
60
+ "(#{s.mesa_mirror}):", :blue
61
+ s.mesa_mirror = response unless response.empty?
62
+
63
+ # Get location of source MESA work (where testing happens)
64
+ response = shell.ask "Where is/should your working directory for "\
65
+ "testing be located? This is where testing actually occurs, but all "\
66
+ "files it uses are cached in the mirror repo to save time later. " \
67
+ "(#{s.mesa_work}):", :blue
68
+ s.mesa_work = response unless response.empty?
69
+
54
70
  # Get platform information
55
71
  response = shell.ask 'What is the platform of this computer (eg. ' \
56
72
  "macOS, Ubuntu)? (#{s.platform}):", :blue
57
73
  s.platform = response unless response.empty?
58
- response = shell.ask 'What is the version of the platform (eg. 10.13, ' \
59
- "16.04)? (#{s.platform_version}):", :blue
74
+ response = shell.ask 'What is the version of the platform (eg. 10.15.5, ' \
75
+ "Ubuntu 16.04)? (#{s.platform_version}):", :blue
60
76
  s.platform_version = response unless response.empty?
61
77
 
62
- # Get processor information
63
- response = shell.ask 'What type of processor does this computer have ' \
64
- "(eg. 3.1 GHz Intel i7)? (#{s.processor}):", :blue
65
- s.processor = response unless response.empty?
66
-
67
- # Get ram information
68
- response = shell.ask 'How much RAM (in integer GB) does this computer ' \
69
- "have (eg. 8)? (#{s.ram_gb}) ", :blue
70
- s.ram_gb = response.to_i unless response.empty?
71
-
72
- # Get compiler information
73
- response = shell.ask "Which compiler are you using? (#{s.compiler}):",
74
- :blue, limited_to: ['', 'SDK', 'gfortran', 'ifort']
75
- s.compiler = response unless response.empty?
76
-
77
- # Get compiler version
78
- response = shell.ask 'What version of the compiler (eg. 20170921 or ' \
79
- "7.2.0)? (#{s.compiler_version}): ", :blue
80
- s.compiler_version = response unless response.empty?
81
-
82
- # Get earliest revision to check
83
- response = shell.ask "What's the earliest revision to search back to " \
84
- 'when finding the latest testable revision (eg. 10000)? ' \
85
- "(#{s.last_tested}): ", :blue
86
- s.last_tested = response.to_i unless response.empty?
87
-
88
78
  # Confirm save location
89
79
  response = shell.ask "This will be saved in #{s.config_file}. Press " \
90
80
  'enter to accept or enter a new location:', :blue, path: true
@@ -95,15 +85,15 @@ e-mail and password will be stored in plain text.'
95
85
  if confirm_computer_data
96
86
  save_computer_data
97
87
  else
98
- puts "Restarting wizard.\n"
88
+ shell.say "Restarting wizard.\n"
99
89
  setup
100
90
  end
101
91
  end
102
92
 
103
93
  def self.new_from_config(
104
- config_file: File.join(ENV['HOME'], '.mesa_test.yml'), force_setup: false,
94
+ config_file: File.join(ENV['HOME'], '.mesa_test', 'config.yml'),
95
+ force_setup: false,
105
96
  base_uri: DEFAULT_URI
106
- # base_uri: 'http://localhost:3000'
107
97
  )
108
98
  new_submitter = new(config_file: config_file, base_uri: base_uri)
109
99
  if force_setup
@@ -117,22 +107,27 @@ e-mail and password will be stored in plain text.'
117
107
  end
118
108
 
119
109
  attr_accessor :computer_name, :user_name, :email, :password, :platform,
120
- :platform_version, :processor, :ram_gb, :compiler,
121
- :compiler_version, :config_file, :base_uri, :last_tested
110
+ :mesa_mirror, :mesa_work, :platform_version, :processor,
111
+ :config_file, :base_uri, :last_tested, :github_protocol
122
112
 
123
113
  attr_reader :shell
124
114
 
125
115
  # many defaults are set in body
126
116
  def initialize(
127
- computer_name: nil, user_name: nil, email: nil, platform: nil,
128
- platform_version: nil, processor: nil, ram_gb: nil, compiler: nil,
129
- compiler_version: nil, config_file: nil, base_uri: nil, last_tested: nil
117
+ computer_name: nil, user_name: nil, email: nil, github_protocol: nil,
118
+ mesa_mirror: nil, platform: nil, platform_version: nil, processor: nil,
119
+ config_file: nil, base_uri: nil, last_tested: nil
130
120
  )
131
121
  @computer_name = computer_name || Socket.gethostname.scan(/^[^\.]+\.?/)[0]
132
122
  @computer_name.chomp!('.') if @computer_name
133
123
  @user_name = user_name || (ENV['USER'] || ENV['USERNAME'])
134
124
  @email = email || ''
135
125
  @password = password || ''
126
+ @github_protocol = github_protocol || :ssh
127
+ @mesa_mirror = mesa_mirror ||
128
+ File.join(ENV['HOME'], '.mesa_test', 'mirror')
129
+ @mesa_work = mesa_work ||
130
+ File.join(ENV['HOME'], '.mesa_test', 'work')
136
131
  @platform = platform
137
132
  if @platform.nil?
138
133
  @platform =
@@ -146,17 +141,15 @@ e-mail and password will be stored in plain text.'
146
141
  end
147
142
  @platform_version = platform_version || ''
148
143
  @processor = processor || ''
149
- @ram_gb = ram_gb || 0
150
- @compiler = compiler || 'SDK'
151
- @compiler_version = compiler_version || ''
152
- @config_file = config_file || File.join(ENV['HOME'], '.mesa_test.yml')
144
+ @config_file = config_file || File.join(ENV['HOME'], '.mesa_test',
145
+ 'config.yml')
153
146
  @base_uri = base_uri
154
- @last_tested = last_tested || DEFAULT_REVISION
155
147
 
156
148
  # set up thor-proof way to get responses from user. Thor hijacks the
157
149
  # gets command, so we have to use its built-in "ask" method, which is
158
150
  # actually more useful
159
151
  @shell = Thor::Shell::Color.new
152
+
160
153
  yield self if block_given?
161
154
  end
162
155
 
@@ -169,14 +162,12 @@ e-mail and password will be stored in plain text.'
169
162
  puts 'Ready to submit the following data:'
170
163
  puts '-------------------------------------------------------'
171
164
  puts "Computer Name #{computer_name}"
172
- puts "User Name #{user_name}"
173
165
  puts "User email #{email}"
174
166
  puts 'Password ***********'
167
+ puts "GitHub Protocol #{github_protocol}"
168
+ puts "MESA Mirror Location #{mesa_mirror}"
169
+ puts "MESA Work Location #{mesa_work}"
175
170
  puts "Platform #{platform} #{platform_version}"
176
- puts "Processor #{processor}"
177
- puts "RAM #{ram_gb} GB"
178
- puts "Compiler #{compiler} #{compiler_version}"
179
- puts "Last tested revision #{last_tested}"
180
171
  puts "Config location #{config_file}"
181
172
  puts '-------------------------------------------------------'
182
173
  puts ''
@@ -189,151 +180,107 @@ e-mail and password will be stored in plain text.'
189
180
  # change platforms (i.e. switch from mac to linux, or change between linux
190
181
  # flavors), you should create a new computer account. Similarly, create new
191
182
  # computer accounts if you change your RAM or processor. You do not need
192
- # to change computers if you upgrade your platform (macOS 10.12 -> 10.13) or
193
- # if you try different compilers
194
- #
195
- # Note this is NOT checked! The server really only uses the test-by-test
196
- # quantities (platform version, compiler, compiler version) and the
197
- # computer name. Once the computer is found (by the name) all the other
198
- # data is assumed to be fixed. The others... probably shouldn't be here,
199
- # but remain so you can confirm that the computer on the web server is the
200
- # same one you think you are working with locally.
183
+ # to change computers if you upgrade your platform (macOS 10.12 -> 10.13
201
184
  def save_computer_data
202
185
  data_hash = {
203
186
  'computer_name' => computer_name,
204
- 'user_name' => user_name,
205
187
  'email' => email,
206
188
  'password' => password,
189
+ 'github_protocol' => github_protocol,
190
+ 'mesa_mirror' => mesa_mirror,
191
+ 'mesa_work' => mesa_work,
207
192
  'platform' => platform,
208
- 'processor' => processor,
209
- 'ram_gb' => ram_gb,
210
193
  'platform_version' => platform_version,
211
- 'compiler' => compiler,
212
- 'compiler_version' => compiler_version,
213
- 'last_tested' => last_tested
214
194
  }
195
+ # make sure there's a directory to write to
196
+ unless dir_or_symlink_exists? File.dirname(config_file)
197
+ FileUtils.mkdir_p File.dirname(config_file)
198
+ end
215
199
  File.open(config_file, 'w') { |f| f.write(YAML.dump(data_hash)) }
216
200
  end
217
201
 
218
202
  def load_computer_data
219
203
  data_hash = YAML.safe_load(File.read(config_file), [Symbol])
220
204
  @computer_name = data_hash['computer_name']
221
- @user_name = data_hash['user_name']
222
205
  @email = data_hash['email']
223
206
  @password = data_hash['password']
207
+ @github_protocol = data_hash['github_protocol'].to_sym
208
+ @mesa_mirror = data_hash['mesa_mirror']
209
+ @mesa_work = data_hash['mesa_work']
224
210
  @platform = data_hash['platform']
225
- @processor = data_hash['processor']
226
- @ram_gb = data_hash['ram_gb']
227
211
  @platform_version = data_hash['platform_version']
228
- @compiler = data_hash['compiler']
229
- @compiler_version = data_hash['compiler_version']
230
- @last_tested = data_hash['last_tested'] || @last_tested
231
212
  end
232
213
 
233
- # create and return hash of parameters for a TestInstance submission
234
- # Note: prefer test case's self-reported compiler and compiler version over
235
- # user reported
236
- def submit_params(test_case)
237
- res = {
238
- test_case: test_case.test_name,
239
- mod: test_case.mod,
240
- computer: computer_name,
214
+ # Parameters to be submitted in JSON format for reporting information about
215
+ # the submitting user and computer
216
+ def submitter_params
217
+ {
241
218
  email: email,
242
219
  password: password,
243
- runtime_seconds: test_case.runtime_seconds,
244
- re_time: test_case.re_time,
245
- total_runtime_seconds: test_case.total_runtime_seconds,
246
- mesa_version: test_case.mesa_version,
247
- passed: test_case.passed? ? 1 : 0,
248
- compiler: test_case.compiler || compiler,
249
- compiler_version: test_case.compiler_version || compiler_version,
250
- platform_version: platform_version,
251
- omp_num_threads: test_case.test_omp_num_threads,
252
- success_type: test_case.success_type,
253
- failure_type: test_case.failure_type,
254
- steps: test_case.steps,
255
- retries: test_case.retries,
256
- backups: test_case.backups,
257
- diff: test_case.diff,
258
- checksum: test_case.checksum,
259
- rn_mem: test_case.rn_mem,
260
- re_mem: test_case.re_mem,
261
- summary_text: test_case.summary_text
220
+ computer: computer_name,
221
+ platform_version: platform_version
262
222
  }
263
-
264
- # enter in test-specific data, DISABLED FOR NOW
265
- # test_case.data_names.each do |data_name|
266
- # unless test_case.data[data_name].nil?
267
- # res[data_name] = test_case.data[data_name]
268
- # end
269
- # end
270
- res
271
223
  end
272
224
 
273
- def revision_submit_params(mesa)
274
- # only query svn if we didn't do it in the first place. Probably
275
- # unnecessary
276
- mesa.load_svn_data if mesa.use_svn? && mesa.svn_version.nil?
277
- # version gives data about version
278
- # user gives data about the user and computer submitting information
279
- # instances is array of hashes that identify test instances (more below)
280
- res = {
281
- version: {number: mesa.version_number, compiled: mesa.installed?},
282
- user: {email: email, password: password, computer: computer_name},
283
- instances: []
284
- }
285
- if mesa.use_svn?
286
- res[:version][:author] = mesa.svn_author
287
- res[:version][:log] = mesa.svn_log
288
- end
289
-
290
- # bail out if installation failed (and we care)
291
- return [res, []] unless res[:version][:compiled]
292
-
293
- # Successfully compiled, now gather test instance data.
294
-
295
- # hold on to test case names that fail in synthesizing params
225
+ # Parameters to be submitted in JSON format for reporting information about
226
+ # the overall commit being tested; used even if only submitting an entire
227
+ # test. This also determines if the submission is for an entire commit
228
+ # (compilation information and every test), an empty commit (just
229
+ # compilation information), or a non-empty, but also non-entire submission
230
+ # (results for a single test without compilation information)
231
+ def commit_params(mesa, entire: true, empty: false)
232
+ # the compiler data should be able to be used as-is, but right now the
233
+ # names don't match with what the database expects, so we do some renaming
234
+ # shenanigans.
235
+ #
236
+ ####################################
237
+ # THIS SHOULD GO BEFORE PRODUCTION #
238
+ {
239
+ sha: mesa.sha,
240
+ compiled: mesa.installed?,
241
+ entire: entire,
242
+ empty: empty,
243
+ }.merge(mesa.compiler_hash)
244
+ end
245
+
246
+ # Given a valid +Mesa+ object, create an array of hashes that describe the
247
+ # test cases and the test results. These will be encoded as an array of
248
+ # JSON objects.
249
+ def instance_params(mesa)
296
250
  has_errors = []
297
-
298
- # each instance has basic information in :test_instance and extra
299
- # information that requires the web app to work, stored in :extra
300
- mesa.test_names.each do |mod, names|
251
+ res = []
252
+ mesa.test_case_names.each do |mod, names|
301
253
  names.each do |test_name|
302
254
  begin
303
255
  test_case = mesa.test_cases[mod][test_name]
304
- res[:instances] << {
305
- test_instance: {
306
- runtime_seconds: test_case.runtime_seconds,
307
- re_time: test_case.re_time,
308
- total_runtime_seconds: test_case.total_runtime_seconds,
309
- passed: test_case.passed?,
310
- compiler: test_case.compiler || compiler,
311
- compiler_version: test_case.compiler_version || compiler_version,
312
- platform_version: platform_version,
313
- omp_num_threads: test_case.test_omp_num_threads,
314
- success_type: test_case.success_type,
315
- failure_type: test_case.failure_type,
316
- steps: test_case.steps,
317
- retries: test_case.retries,
318
- backups: test_case.backups,
319
- diff: test_case.diff,
320
- checksum: test_case.checksum,
321
- rn_mem: test_case.rn_mem,
322
- re_mem: test_case.re_mem,
323
- summary_text: test_case.summary_text
324
- },
325
- extra: { test_case: test_name, mod: mod }
326
- }
256
+ res << test_case.results_hash
327
257
  rescue TestCaseDirError
328
- shell.say "Passage status for #{test_case.test_name} not yet "\
329
- 'known. Run test first and then submit.', :red
258
+ # shell.say "It appears that #{test_case.test_name} has not been "\
259
+ # 'run yet. Unable to submit data for this test.', :red
330
260
  has_errors << test_case
331
261
  end
332
262
  end
333
263
  end
334
- [res, has_errors]
264
+ unless has_errors.empty?
265
+ shell.say "The following test cases could NOT be read for submission:",
266
+ :red
267
+ has_errors.each do |test_case|
268
+ shell.say "- #{test_case.test_name}", :red
269
+ end
270
+ end
271
+ res
272
+ end
273
+
274
+ # Parameters for a single test case. +mesa+ is an instance of +Mesa+, and
275
+ # +test_case+ is an instance of MesaTestCase representing the test case to
276
+ # be submitted
277
+ def single_instance_params(test_case)
278
+ [test_case.results_hash]
335
279
  end
336
280
 
281
+ # Phone home to testhub and confirm that computer and user are valid. Useful
282
+ # for confirming that submissions will be accepted before wasting time on a
283
+ # test later.
337
284
  def confirm_computer
338
285
  uri = URI.parse(base_uri + '/check_computer.json')
339
286
  https = Net::HTTP.new(uri.hostname, uri.port)
@@ -350,12 +297,10 @@ e-mail and password will be stored in plain text.'
350
297
  JSON.parse(https.request(request).body).to_hash
351
298
  end
352
299
 
353
- # attempt to post to MesaTestHub with test_case parameters
354
- # returns true if the id is in the returned JSON (indicating success)
355
- # otherwise returns false (maybe failed in authorization or in finding
356
- # computer or test case) No error thrown for failure, though.
357
- def submit(test_case)
358
- uri = URI.parse(base_uri + '/test_instances/submit.json')
300
+ # submit entire commit's worth of test cases, OR submit compilation status
301
+ # and NO test cases
302
+ def submit_commit(mesa, empty: false)
303
+ uri = URI.parse(base_uri + '/submissions/create.json')
359
304
  https = Net::HTTP.new(uri.hostname, uri.port)
360
305
  https.use_ssl = true if base_uri.include? 'https'
361
306
 
@@ -363,74 +308,38 @@ e-mail and password will be stored in plain text.'
363
308
  uri,
364
309
  initheader = { 'Content-Type' => 'application/json' }
365
310
  )
366
- begin
367
- request.body = submit_params(test_case).to_json
368
- rescue TestCaseDirError
369
- shell.say "\nPassage status for #{test_case.test_name} not yet known. " \
370
- 'Run test first and then submit.', :red
371
- return false
372
- end
373
311
 
374
- # verbose = true
375
- # puts "\n" if verbose
376
- # puts JSON.parse(request.body).to_hash if verbose
312
+ # create the request body for submission to the submissions API
313
+ #
314
+ # if we have an empty submission, then it is necessarily not entire.
315
+ # Similarly, a non-empty submission is necessarily entire (otherwise one
316
+ # would use +submit_instance+)
317
+ request_data = {submitter: submitter_params,
318
+ commit: commit_params(mesa, empty: empty, entire: !empty)}
319
+ # don't need test instances if it's an empty submission or if compilation
320
+ # failed
321
+ if !empty && request_data[:commit][:compiled]
322
+ request_data[:instances] = instance_params(mesa)
323
+ end
324
+ request.body = request_data.to_json
377
325
 
326
+ # actually do the submission
378
327
  response = https.request request
379
- # puts JSON.parse(response.body).to_hash if verbose
380
- response.is_a? Net::HTTPCreated
381
- end
382
328
 
383
- def submit_all(mesa, mod = :all)
384
- submitted_cases = []
385
- unsubmitted_cases = []
386
- if mod == :all
387
- success = true
388
- mesa.test_names.each_key do |this_mod|
389
- success &&= submit_all(mesa, mod = this_mod)
390
- end
329
+ if !response.is_a? Net::HTTPCreated
330
+ shell.say "\nFailed to submit some or all test case instances and/or "\
331
+ 'commit data.', :red
332
+ false
391
333
  else
392
- mesa.test_names[mod].each do |test_name|
393
- # get at test case
394
- test_case = mesa.test_cases[mod][test_name]
395
- # try to submit and note if it does or doesn't successfully submit
396
- submitted = false
397
- submitted = submit(test_case) unless test_case.outcome == :not_tested
398
- if submitted
399
- submitted_cases << test_name
400
- else
401
- unsubmitted_cases << test_name
402
- end
403
- end
404
- puts "\nSubmission results for #{mod} module:"
405
- puts '#####################################'
406
- if !submitted_cases.empty?
407
- shell.say 'Submitted the following cases:', :green
408
- puts submitted_cases.join("\n")
409
- else
410
- shell.say 'Did not successfully submit any cases.', :red
411
- end
412
- unless unsubmitted_cases.empty?
413
- puts "\n\n\n"
414
- shell.say 'Failed to submit the following cases:', :red
415
- puts unsubmitted_cases.join("\n")
416
- end
417
- # return true and update last tested if all cases were submitted
418
- success = submitted_cases.length == mesa.test_names[mod].length
419
- if success
420
- @last_tested = mesa.version_number
421
- shell.say "\n\nUpdating last tested revision to #{last_tested}."
422
- save_computer_data
423
- end
334
+ shell.say "\nSuccessfully submitted commit #{mesa.sha}.", :green
335
+ true
424
336
  end
425
- # return boolean indicating whether or not all cases successfully
426
- # SUBMITTED (irrespective of passing status)
427
- success
428
337
  end
429
338
 
430
- # similar to submit_all, but does EVERYTHING in one post, including
431
- # version information. No support for individual modules now.
432
- def submit_revision(mesa)
433
- uri = URI.parse(base_uri + '/versions/submit_revision.json')
339
+ # submit results for a single test case instance. Does *not* report overall
340
+ # compilation status to testhub. Use an empty commit submission for that
341
+ def submit_instance(mesa, test_case)
342
+ uri = URI.parse(base_uri + '/submissions/create.json')
434
343
  https = Net::HTTP.new(uri.hostname, uri.port)
435
344
  https.use_ssl = true if base_uri.include? 'https'
436
345
 
@@ -438,207 +347,182 @@ e-mail and password will be stored in plain text.'
438
347
  uri,
439
348
  initheader = { 'Content-Type' => 'application/json' }
440
349
  )
441
- request_data, error_cases = revision_submit_params(mesa)
442
- if request_data[:instances].empty? && mesa.installed?
443
- shell.say "No completed test data found in #{mesa.mesa_dir}. Aborting.",
444
- :red
445
- return false
446
- end
447
- request.body = request_data.to_json
448
350
 
449
- # verbose = true
450
- # puts "\n" if verbose
451
- # puts JSON.parse(request.body).to_hash if verbose
351
+ # create the request body for submission to the submissions API
352
+ #
353
+ # submission is not empty (there is one test case), and it is also not
354
+ # entire (... there is only test case)
355
+ request_data = {submitter: submitter_params,
356
+ commit: commit_params(mesa, empty: false, entire: false),
357
+ instances: single_instance_params(test_case)}
358
+ request.body = request_data.to_json
452
359
 
360
+ # actually do the submission
453
361
  response = https.request request
454
- # puts JSON.parse(response.body).to_hash if verbose
362
+
455
363
  if !response.is_a? Net::HTTPCreated
456
- shell.say "\nFailed to submit some or all cases and/or version data.",
457
- :red
458
- false
459
- elsif !error_cases.empty?
460
- shell.say "\nFailed to gather data for the following cases:", :red
461
- error_cases.each { |tc| shell.say " #{tc.test_name}", :red }
364
+ shell.say "\nFailed to submit #{test_case.test_name} for commit "\
365
+ "#{mesa.sha}", :red
462
366
  false
463
367
  else
464
- shell.say "\nSuccessfully submitted revision #{mesa.version_number}.", :green
465
- @last_tested = mesa.version_number
466
- shell.say "\n\nUpdating last tested revision to #{last_tested}."
467
- save_computer_data
468
- true
368
+ shell.say "\nSuccessfully submitted instance of #{test_case.test_name} "\
369
+ "for commit #{mesa.sha}.", :green
370
+ true
469
371
  end
470
372
  end
471
373
  end
472
374
 
473
375
  class Mesa
474
- SVN_URI = 'https://subversion.assembla.com/svn/mesa\^mesa/trunk'.freeze
475
-
476
- attr_reader :mesa_dir, :test_data, :test_names, :test_cases, :shell,
477
- :svn_version, :svn_author, :svn_log, :using_sdk
478
- attr_accessor :update_checksums
479
-
480
- def self.download(version_number: nil, new_mesa_dir: nil, use_svn: true,
481
- using_sdk: true)
482
- new_mesa_dir ||= File.join(ENV['HOME'], 'mesa-test-r' + version_number.to_s)
483
- svn_command = "svn co -r #{version_number} #{SVN_URI} #{new_mesa_dir}"
484
- success = bash_execute(svn_command)
485
- unless success
486
- raise MesaDirError, 'Encountered a problem in downloading mesa ' \
487
- "revision #{version_number}. Perhaps svn isn't " \
488
- 'working properly?' + "\n\n"\
489
- 'Tried the following command: ' + svn_command
490
-
491
- end
492
- Mesa.new(mesa_dir: new_mesa_dir, use_svn: use_svn, using_sdk: using_sdk)
493
- end
494
-
495
- def self.log_since(last_tested = DEFAULT_REVISION)
496
- # svn commit log back to, but excluding, the last revision tested
497
- `svn log #{SVN_URI} -r #{last_tested + 1}:HEAD`
498
- end
499
-
500
- def self.log_lines_since(last_tested = DEFAULT_REVISION)
501
- log_since(last_tested).split("\n").reject(&:empty?)
502
- end
503
-
504
- def self.add_commit(commits, revision, author)
505
- commits << Commit.new
506
- commits.last.revision = revision.to_i
507
- commits.last.author = author
508
- commits.last.message = []
509
- end
510
-
511
- def self.process_line(commits, line)
512
- last = commits.last
513
- if line =~ /^-+$/
514
- # dashed lines separate commits
515
- # Done with last commit (if it exists), so clean up message
516
- last.message = last.message.join("\n") unless last.nil?
517
- elsif line =~ /^r(\d+) \| (\w+) \| .* \| \d+ lines?$/
518
- # first line of a commit, scrape data and make new commit
519
- add_commit(commits, $1, $2)
520
- else
521
- # add lines to the message (will concatenate later to single String)
522
- last.message << line.strip
523
- end
524
- end
525
-
526
- # all commits since the given version number
527
- def self.commits_since(last_tested = DEFAULT_REVISION)
528
- commits = []
529
- log_lines_since(last_tested).each { |line| process_line(commits, line) }
530
- commits.sort_by(&:revision).reverse
531
- end
376
+ attr_reader :mesa_dir, :mirror_dir, :names_to_numbers, :shell,
377
+ :test_case_names, :test_cases, :github_protocol
532
378
 
533
- def self.last_non_paxton_revision(last_tested = DEFAULT_REVISION)
534
- commits_since(last_tested).each do |commit|
535
- return commit.revision unless commit.author == 'bill_paxton'
536
- end
537
- # give out garbage if no valid commit is found
538
- nil
379
+ def self.checkout(sha: nil, work_dir: nil, mirror_dir: nil,
380
+ github_protocol: :ssh)
381
+ m = Mesa.new(mesa_dir: work_dir, mirror_dir: mirror_dir,
382
+ github_protocol: github_protocol)
383
+ m.checkout(new_sha: sha)
384
+ m
539
385
  end
540
386
 
541
- def initialize(mesa_dir: ENV['MESA_DIR'], use_svn: true, using_sdk: true)
387
+ def initialize(mesa_dir: ENV['MESA_DIR'], mirror_dir: nil,
388
+ github_protocol: :ssh)
542
389
  # absolute_path ensures that it doesn't matter where commands are executed
543
390
  # from
544
391
  @mesa_dir = File.absolute_path(mesa_dir)
545
- @use_svn = use_svn
546
- @using_sdk = using_sdk
547
- @update_checksums = false
392
+ @mirror_dir = File.absolute_path(mirror_dir)
393
+
394
+ # don't worry about validity of github protocol until it is needed in a
395
+ # checkout. This way you can have garbage in there if you never really need
396
+ # it.
397
+ @github_protocol = if github_protocol.respond_to? :to_sym
398
+ github_protocol.to_sym
399
+ else
400
+ github_protocol
401
+ end
548
402
 
549
403
  # these get populated by calling #load_test_data
550
- @test_data = {}
551
- @test_names = {}
552
404
  @test_cases = {}
405
+ @test_case_names = {}
406
+ @names_to_numbers = {}
553
407
 
554
408
  # way to output colored text
555
409
  @shell = Thor::Shell::Color.new
556
-
557
- # these can be populated by calling load_svn_data
558
- @svn_version = nil
559
- @svn_author = nil
560
- @svn_log = nil
561
- load_svn_data if use_svn?
562
- end
563
-
564
- def use_svn?
565
- @use_svn
566
410
  end
567
411
 
568
- def determine_diff
569
- # automatically determine if update_checksums should be true (don't do
570
- # diffs or true (DO do diffs). Only works if svn data has ALREADY been
571
- # loaded
572
-
573
- # don't do anything to @update_checksums if we haven't loaded svn data
574
- return unless @svn_log
575
-
576
- # by default, DON'T do diffs
577
- @update_checksums = true
578
-
579
- # list of phrases, which, if present in the log entry, will trigger diffs
580
- [
581
- /updated? checksums?/i,
582
- /checksums? updated?/i,
583
- /ready for diffs?/i,
584
- ].each { |trigger| @update_checksums = false if trigger =~ @svn_log }
585
- if @update_checksums
586
- shell.say "\nFrom svn log, didn't decide to tak diffs."
412
+ def checkout(new_sha: 'HEAD')
413
+ # before anything confirm that git-lfs has been installed
414
+ shell.say "\nEnsuring that git-lfs is installed... ", :blue
415
+ command = 'git lfs help >> /dev/null 2>&1'
416
+ if bash_execute(command)
417
+ shell.say "yes", :green
587
418
  else
588
- shell.say "From svn log, automatically decided to take diffs."
419
+ shell.say "no", :red
420
+ raise(GitHubError, "The command #{command} returned with an error "\
421
+ 'status, indicating that git-lfs is not installed. '\
422
+ 'Make sure it is installed and try again.')
423
+ end
424
+
425
+ # set up mirror if it doesn't exist
426
+ unless dir_or_symlink_exists?(mirror_dir)
427
+ shell.say "\nCreating initial mirror at #{mirror_dir}. "\
428
+ 'This might take awhile...', :blue
429
+ FileUtils.mkdir_p mirror_dir
430
+ case github_protocol
431
+ when :ssh
432
+ command = "git clone --mirror #{GITHUB_SSH} #{mirror_dir}"
433
+ shell.say command
434
+ # fail loudly if this doesn't work
435
+ unless bash_execute(command)
436
+ # nuke the mirror directory since it is probably bogus (make this
437
+ # code fire off the next time checkout is done)
438
+ shell.say "Failed. Removing the [potentially corrupted] mirror.", :red
439
+ command = "rm -rf #{mirror_dir}"
440
+ shell.say command
441
+ bash_execute(command)
442
+
443
+ raise(GitHubError, 'Error while executing the following command:'\
444
+ "#{command}. Perhaps you haven't set up "\
445
+ 'ssh keys with your GitHub account?')
446
+ end
447
+ when :https
448
+ command = "git clone --mirror #{GITHUB_HTTPS} #{mirror_dir}"
449
+ shell.say command
450
+ # fail loudly if this doesn't work
451
+ unless bash_execute(command)
452
+ # nuke the mirror directory since it is probably bogus (make this
453
+ # code fire off the next time checkout is done)
454
+ shell.say "Failed. Removing the [potentially corrupted] mirror.", :red
455
+ command = "rm -rf #{mirror_dir}"
456
+ shell.say command
457
+ bash_execute(command)
458
+
459
+ raise(GitHubError, 'Error while executing the following command: '\
460
+ "#{command}. Perhaps you need to configure "\
461
+ 'global GitHub account settings for https '\
462
+ 'authentication to work properly?')
463
+ end
464
+ else
465
+ raise(GitHubError, "Invalid GitHub protocol: \"#{github_protocol}\"")
466
+ end
589
467
  end
590
- shell.say "log entry: #{@svn_log}"
591
- end
592
468
 
593
- def version_number
594
- version = @svn_version || 0
595
- # fall back to MESA_DIR/data's version number svn didn't work
596
- version = data_version_number unless version > 0
597
- version
469
+ update_mirror
470
+
471
+ # ensure "work" directory is removed from worktree
472
+ remove
473
+
474
+ # create "work" directory with proper commit
475
+ shell.say "\nSetting up worktree repo...", :blue
476
+ FileUtils.mkdir_p mesa_dir
477
+ command = "git -C #{mirror_dir} worktree add #{mesa_dir} #{new_sha}"
478
+ shell.say command
479
+ return if bash_execute(command)
480
+
481
+ raise(GitHubError, 'Failed while executing the following command: '\
482
+ "\"#{command}\".")
598
483
  end
599
484
 
600
- def log_entry
601
- `svn log #{mesa_dir} -r #{version_number}`
485
+ def update_mirror
486
+ shell.say "\nFetching MESA history...", :blue
487
+ command = "git -C #{mirror_dir} fetch origin"
488
+ shell.say command
489
+ # fail loudly
490
+ return if bash_execute(command)
491
+
492
+ raise(GitHubError, 'Failed while executing the following command: '\
493
+ "\"#{command}\".")
602
494
  end
603
495
 
604
- def load_svn_data
605
- # if this number is bad, #version_number will use fallback method
606
- @svn_version = svn_version_number
607
- lines = log_entry.split("\n").reject { |line| line =~ /^-+$/ or line.empty?}
608
- data_line = lines.shift
609
- revision, author, date, length = data_line.split('|')
610
- @svn_author = author.strip
611
- @svn_log = lines.join("\n").strip
496
+ def remove
497
+ return unless File.exist? mesa_dir
498
+ shell.say "\nRemoving work directory from worktree (clearing old data)...",
499
+ :blue
500
+ command = "git -C #{mirror_dir} worktree remove --force #{mesa_dir}"
501
+ shell.say command
502
+ return if bash_execute(command)
503
+
504
+ shell.say "Failed. Simply trying to remove the directory.", :red
505
+ command = "rm -rf #{mesa_dir}"
506
+ shell.say command
507
+ # fail loudly (the "true" tells bash_execute to raise an exception if
508
+ # the command fails)
509
+ bash_execute(command, true)
612
510
  end
613
511
 
614
- # get version number from svn (preferred method)
615
- def svn_version_number
616
- # match output of svn info to a line with the revision, capturing the
617
- # number, and defaulting to 0 if none is found.
618
- matches = /Revision\:\s+(\d+)/.match(`svn info #{mesa_dir}`)
619
- unless matches.nil?
620
- return matches[1].to_i
621
- end
622
- return 0
623
- rescue Errno::ENOENT
624
- return 0
512
+ def git_sha
513
+ bashticks("git -C #{mesa_dir} rev-parse HEAD")
625
514
  end
626
515
 
627
- # read version number from $MESA_DIR/data/version_number
628
- def data_version_number
629
- contents = ''
630
- File.open(File.join(mesa_dir, 'data', 'version_number'), 'r') do |f|
631
- contents = f.read
632
- end
633
- contents.strip.to_i
516
+ def sha
517
+ git_sha
634
518
  end
635
519
 
636
520
  def clean
637
521
  with_mesa_dir do
638
522
  visit_and_check mesa_dir, MesaDirError, 'E\countered a problem in ' \
639
523
  "running `clean` in #{mesa_dir}." do
640
- puts 'MESA_DIR = ' + ENV['MESA_DIR']
641
- puts './clean'
524
+ shell.say('MESA_DIR = ' + ENV['MESA_DIR'])
525
+ shell.say './clean'
642
526
  bash_execute('./clean')
643
527
  end
644
528
  end
@@ -649,8 +533,8 @@ class Mesa
649
533
  with_mesa_dir do
650
534
  visit_and_check mesa_dir, MesaDirError, 'Encountered a problem in ' \
651
535
  "running `install` in #{mesa_dir}." do
652
- puts 'MESA_DIR = ' + ENV['MESA_DIR']
653
- puts './install'
536
+ shell.say('MESA_DIR = ' + ENV['MESA_DIR'])
537
+ shell.say './install'
654
538
  bash_execute('./install')
655
539
  end
656
540
  end
@@ -661,20 +545,31 @@ class Mesa
661
545
 
662
546
  # throw an error unless it seems like it's properly compiled
663
547
  def check_installation
664
- unless installed?
665
- raise MesaDirError, 'Installation check failed (no .mod files found ' \
666
- 'in the last compiled module).'
548
+ return if installed?
549
+
550
+ raise MesaDirError, 'Installation check failed (build.log doesn\'t '\
551
+ 'show a successful installation).'
552
+ end
553
+
554
+ # sourced from $MESA_DIR/testhub.yml, which should be created after
555
+ # installation
556
+ def compiler_hash
557
+ data_file = File.join(mesa_dir, 'testhub.yml')
558
+ unless File.exist? data_file
559
+ raise(MesaDirError, "Could not find file testhub.yml in #{mesa_dir}.")
667
560
  end
668
- end
669
561
 
670
- def destroy
671
- FileUtils.rm_rf mesa_dir
562
+ res = YAML.safe_load(File.read(data_file))
563
+ # currently version_number is reported, but we don't need that in Git land
564
+ res.delete('version_number') # returns the value, not the updated hash
565
+ res
672
566
  end
673
567
 
674
568
  ## TEST SUITE METHODS
675
569
 
676
570
  def check_mod(mod)
677
571
  return if MesaTestCase.modules.include? mod
572
+
678
573
  raise TestCaseDirError, "Invalid module: #{mod}. Must be one of: " +
679
574
  MesaTestCase.modules.join(', ')
680
575
  end
@@ -694,53 +589,32 @@ class Mesa
694
589
  end
695
590
  else
696
591
  check_mod mod
697
- # load data from the source file
698
- source_lines = IO.readlines(
699
- File.join(test_suite_dir(mod: mod), 'do1_test_source')
700
- )
701
592
 
702
- # initialize data hash to empty hash and name array to empty array
703
- @test_data[mod] = {}
704
- @test_names[mod] = []
593
+ # convert output of +list_tests+ to a dictionary that maps
594
+ # names to numbers since +each_test_run+ only knows about numbers
595
+ @names_to_numbers[mod] = {}
596
+ @test_case_names[mod] = []
705
597
  @test_cases[mod] = {}
706
-
707
- # read through each line and find four data, name, success string, final
708
- # model name, and photo. Either of model name and photo can be "skip"
709
- source_lines.each do |line|
710
- no_skip = /^do_one (.+)\s+"([^"]*)"\s+"([^"]+)"\s+(x?\d+|auto)/
711
- one_skip = /^do_one (.+)\s+"([^"]*)"\s+"([^"]+)"\s+skip/
712
- two_skip = /^do_one (.+)\s+"([^"]*)"\s+skip\s+skip/
713
- found_test = false
714
- if line =~ no_skip
715
- found_test = true
716
- @test_data[mod][$1] = { success_string: $2, final_model: $3,
717
- photo: $4}
718
- elsif line =~ one_skip
719
- found_test = true
720
- @test_data[mod][$1] = { success_string: $2, final_model: $3,
721
- photo: nil }
722
- elsif line =~ two_skip
723
- found_test = true
724
- @test_data[mod][$1] = { success_string: $2, final_model: nil,
725
- photo: nil }
726
- end
727
-
728
- if found_test
729
- @test_names[mod] << $1 unless @test_names[mod].include? $1
598
+ visit_dir(test_suite_dir(mod: mod), quiet: true) do
599
+ bashticks('./list_tests').split("\n").each do |line|
600
+ num, tc_name = line.strip.split
601
+ @names_to_numbers[mod][tc_name.strip] = num.to_i
602
+ @test_case_names[mod] << tc_name.strip
603
+ @test_cases[mod][tc_name.strip] = MesaTestCase.new(
604
+ test: tc_name.strip,
605
+ mod: mod,
606
+ position: num.to_i,
607
+ mesa: self
608
+ )
730
609
  end
731
610
  end
732
-
733
- # make MesaTestCase objects accessible by name
734
- @test_names[mod].each do |test_name|
735
- data = @test_data[mod][test_name]
736
- @test_cases[mod][test_name] = MesaTestCase.new(
737
- test: test_name, mesa: self, success_string: data[:success_string],
738
- mod: mod, final_model: data[:final_model], photo: data[:photo]
739
- )
740
- end
741
611
  end
742
612
  end
743
613
 
614
+ def test_case_count(mod: :all)
615
+ all_names_ordered(mod: mod).count
616
+ end
617
+
744
618
  # can accept a number (in string form) as a name for indexed access
745
619
  def find_test_case(test_case_name: nil, mod: :all)
746
620
  if /\A[0-9]+\z/ =~ test_case_name
@@ -750,44 +624,16 @@ class Mesa
750
624
  end
751
625
  end
752
626
 
753
- # based off of `$MESA_DIR/star/test_suite/each_test_run_and_diff` from
754
- # revision 10000
755
- def each_test_clean(mod: :all)
756
- if mod == :all
757
- MesaTestCase.modules.each { |this_mod| each_test_clean mod: this_mod }
758
- else
759
- check_mod mod
760
- test_names[mod].each do |test_name|
761
- test_cases[mod][test_name].clean
762
- end
763
- end
764
- end
765
-
766
- def each_test_run_and_diff(mod: :all, log_results: false)
627
+ def each_test_run(mod: :all)
767
628
  check_installation
768
- each_test_clean(mod: mod)
769
-
770
- if mod == :all
771
- MesaTestCase.modules.each do |this_mod|
772
- each_test_run_and_diff(mod: this_mod, log_results: log_results)
773
- end
774
- else
775
- test_names[mod].each do |test_name|
776
- test_cases[mod][test_name].do_one
777
- test_cases[mod][test_name].log_results if log_results
778
- end
779
- log_summary(mod: mod) if log_results
780
- end
781
- end
782
629
 
783
- def each_test_load_results(mod: :all)
784
630
  if mod == :all
785
631
  MesaTestCase.modules.each do |this_mod|
786
- each_test_load_results(mod: this_mod)
632
+ each_test_run(mod: this_mod)
787
633
  end
788
634
  else
789
- test_names[mod].each do |test_name|
790
- test_cases[mod][test_name].load_results
635
+ visit_dir(test_suite_dir(mod: mod)) do
636
+ bash_execute('./each_test_run')
791
637
  end
792
638
  end
793
639
  end
@@ -797,34 +643,21 @@ class Mesa
797
643
  end
798
644
 
799
645
  def installed?
800
- # look for output files in the last-installed module
801
- # this isn't perfect, but it's a pretty good indicator of completing
802
- # installation
803
- install_file = File.join(mesa_dir, 'install')
804
- # match last line of things like "do_one SOME_MODULE" or "do_one_parallel
805
- # SOME_MODULE", after which the "SOME_MODULE" will be stored in $1
806
- # that is the last module to be compiled by ./install.
807
- IO.readlines(install_file).select do |line|
808
- line =~ /^\s*do_one\w*\s+\w+/
809
- end.last =~ /^\s*do_one\w*\s+(\w+)/
810
- # module is "installed" if there is a nonzero number of files in the
811
- # module's make directory of the form SOMETHING.mod
812
- !Dir.entries(File.join(mesa_dir, $1, 'make')).select do |file|
813
- File.extname(file) == '.mod'
814
- end.empty?
646
+ # assume build log reflects installation status; does not account for
647
+ # mucking with modules after the fact
648
+ downloaded? && File.read(File.join(mesa_dir, 'build.log')).include?(
649
+ 'MESA installation was successful'
650
+ )
815
651
  end
816
652
 
817
-
818
653
  private
819
654
 
820
- # verify that mesa_dir is valid by checking for version number and test_suite
821
- # directory
655
+ # verify that mesa_dir is valid by checking for existence of test_suite
656
+ # directory for each module (somewhat arbitrary)
822
657
  def check_mesa_dir
823
- res = File.exist?(File.join(mesa_dir, 'data', 'version_number'))
824
- MesaTestCase.modules.each do |mod|
825
- res &&= File.directory?(test_suite_dir(mod: mod))
658
+ MesaTestCase.modules.inject(true) do |res, mod|
659
+ res && dir_or_symlink_exists?(test_suite_dir(mod: mod))
826
660
  end
827
- res
828
661
  end
829
662
 
830
663
  # change MESA_DIR for the execution of the block and then revert to the
@@ -845,49 +678,64 @@ class Mesa
845
678
  end
846
679
  end
847
680
 
848
- def log_summary(mod: :all)
681
+ def all_names_ordered(mod: :all)
682
+ load_test_source_data unless @names_to_numbers
849
683
  if mod == :all
850
- MesaTestCase.modules.each do |this_mod|
851
- log_summary(mod: this_mod)
684
+ # build up list by first constructing each modules list and then
685
+ # concatenating them
686
+ MesaTestCase.modules.inject([]) do |res, this_mod|
687
+ res += all_names_ordered(mod: this_mod)
852
688
  end
853
689
  else
854
690
  check_mod mod
855
- res = []
856
- test_names[mod].each do |test_name|
857
- test_case = test_cases[mod][test_name]
858
- res << {
859
- 'test_name' => test_case.test_name,
860
- 'outcome' => test_case.outcome,
861
- 'failure_type' => test_case.failure_type,
862
- 'success_type' => test_case.success_type,
863
- 'runtime_seconds' => test_case.runtime_seconds,
864
- 'omp_num_threads' => test_case.test_omp_num_threads,
865
- 'mesa_version' => test_case.mesa_version
866
- }
867
- end
868
- summary_file = File.join(test_suite_dir(mod: mod), 'test_summary.yml')
869
- File.open(summary_file, 'w') do |f|
870
- f.write(YAML.dump(res))
691
+ res = Array.new(@names_to_numbers[mod].length, '')
692
+
693
+ # values of the hash give their order, keys are the names, so
694
+ # we assign keys to positions in the array according to their value
695
+ @names_to_numbers[mod].each_pair do |key, val|
696
+ res[val - 1] = key # +list_tests+ gives 1-indexed positions
871
697
  end
698
+ res
872
699
  end
873
700
  end
874
701
 
875
702
  def find_test_case_by_name(test_case_name: nil, mod: :all)
703
+ load_test_source_data unless @names_to_numbers
876
704
  if mod == :all
877
- # look through all loaded modules for desired test case name, return
878
- # FIRST found (assuming no name duplication across modules)
879
- @test_names.each do |this_mod, mod_names|
880
- if mod_names.include? test_case_name
881
- return @test_cases[this_mod][test_case_name]
705
+ # look through all loaded modules for desired test case name, only
706
+ # return a test case if a single case is found with that name
707
+ case all_names_ordered.count(test_case_name)
708
+ when 1
709
+ # it exists in exactly one module, but we need to find the module
710
+ # and then return the +MesaTestCase+ object
711
+ MesaTestCase.modules.each do |this_mod|
712
+ if @test_case_names[this_mod].include? test_case_name
713
+ # found it, return the appropriate object
714
+ return @test_cases[this_mod][test_case_name]
715
+ end
882
716
  end
717
+ raise 'Weird problem: found test case in overall names, but '\
718
+ "not in any particular module. This shouldn't happen."
719
+ when 0
720
+ raise(TestCaseDirError, "Could not find test case #{test_case_name} "\
721
+ 'in any module.')
722
+ else
723
+ raise(TestCaseDirError, 'Found multiple test cases named '\
724
+ "#{test_case_name} in multiple modules. Indicate the module you "\
725
+ 'want to search.')
883
726
  end
884
- # didn't find any matches, return nil
885
- nil
727
+ # append this array to the end of the exisitng one
886
728
  else
887
729
  # module specified; check it and return the proper test case (may be nil
888
730
  # if the test case doesn't exist)
889
731
  check_mod mod
890
- @test_cases[mod][test_case_name]
732
+ if @test_case_names[mod].include? test_case_name
733
+ # happy path: test case exists in the specified module
734
+ return @test_cases[mod][test_case_name]
735
+ else
736
+ raise TestCaseDirError.new('Could not find test case ' \
737
+ "#{test_case_name} in the #{mod} module.")
738
+ end
891
739
  end
892
740
  end
893
741
 
@@ -895,149 +743,69 @@ class Mesa
895
743
  # this will be the index in the name array of the proper module of
896
744
  # the desired test case
897
745
  # input numbers are 1-indexed, but we'll fix that later
898
- return nil if test_number < 1
899
- i = test_number
746
+ if test_number < 1 || test_number > test_case_count(mod: mod)
747
+ raise TestCaseDirError.new('Invalid test case number for searching '\
748
+ "in module #{mod}. Must be between 1 and #{test_case_count(mod: mod)}.")
749
+ end
900
750
 
901
751
  if mod == :all
902
- # search through each module in order
903
- MesaTestCase.modules.each do |this_mod|
904
- # if i is a valid index for names of this module, extract the proper
905
- # test case taking into account that the given i is 1-indexed
906
- if i <= @test_names[this_mod].length
907
- # puts "i = #{i} <= #{@test_names[this_mod].length}"
908
- # @test_names[this_mod].each_with_index do |test_name, i|
909
- # puts sprintf("%-4d", i + 1) + test_name
910
- # end
911
- return find_test_case_by_name(
912
- test_case_name: @test_names[this_mod][i - 1],
913
- mod: this_mod
752
+ # can get the name easily, now need to find the module
753
+ test_case_name = all_names_ordered[test_number - 1]
754
+ MesaTestCase.modules.each do |mod|
755
+ if test_number <= test_case_count(mod: mod)
756
+ # test must live in this module; we have everything
757
+ return MesaTestCase.new(
758
+ test: test_case_name,
759
+ mod: mod,
760
+ mesa: self,
761
+ position: @names_to_numbers[mod][test_case_name]
914
762
  )
763
+ else
764
+ # number was too big, so decrement by this modules case count
765
+ # and move on to next one
766
+ test_number -= test_case_count(mod: mod)
915
767
  end
916
- # index lies outside possible range for this module, move on to
917
- # next module and decrement index by the number of test cases in this
918
- # module
919
- i -= @test_names[this_mod].length
920
768
  end
921
- # return nil if we never broke out of the loop
922
- nil
769
+ # should return before we get here, but fail hard if we do
770
+ raise TestCaseDirError.new('Unknown problem in loading test case #' +
771
+ test_number + '.')
923
772
  else
924
- # module was specified, so just hope things work out for the number
925
- # should probably add a check that the index is actually in the array,
926
- # but if you're using this feature, you probably know what you're doing,
927
- # right? Right?
928
- return find_test_case_by_name(
929
- test_case_name: @test_names[mod][i - 1],
930
- mod: mod
773
+ # module was specified, so we can get at everything right away
774
+ check_mod mod
775
+ return MesaTestCase.new(
776
+ test: all_names_ordered(mod: mod)[test_number - 1],
777
+ mod: mod,
778
+ mesa: self,
779
+ position: test_number
931
780
  )
932
781
  end
933
782
  end
934
783
  end
935
784
 
936
785
  class MesaTestCase
937
- attr_reader :test_name, :mesa_dir, :mesa, :success_string, :final_model,
938
- :failure_msg, :success_msg, :photo, :runtime_seconds,
939
- :test_omp_num_threads, :mesa_version, :shell, :mod, :retries,
940
- :backups, :steps, :runtime_minutes, :summary_text, :compiler,
941
- :compiler_version, :diff, :checksum, :rn_mem, :re_mem,
942
- :re_time, :total_runtime_seconds
943
- attr_accessor :data_names, :data_types, :failure_type, :success_type,
944
- :outcome
786
+ attr_reader :test_name, :mesa, :mod, :position, :shell
945
787
 
946
788
  def self.modules
947
- %i[star binary]
789
+ %i[star binary astero]
948
790
  end
949
791
 
950
- def initialize(test: nil, mesa: nil, success_string: '',
951
- final_model: 'final.mod', photo: nil, mod: nil)
792
+ def initialize(test: nil, mesa: nil, mod: nil, position: nil)
952
793
  @test_name = test
953
- @mesa_dir = mesa.mesa_dir
954
794
  @mesa = mesa
955
- @mesa_version = mesa.version_number
956
- @success_string = success_string
957
- @final_model = final_model
958
- @photo = photo
959
- @failure_type = nil
960
- @success_type = nil
961
- @outcome = :not_tested
962
- @runtime_seconds = 0
963
- @test_omp_num_threads = 1
964
- @runtime_minutes = 0
965
- @total_runtime_seconds = 0
966
- @retries = 0
967
- @backups = 0
968
- @steps = 0
969
- # 2 (default) means uknown. Updated by running or loading data.
970
- # 1 means did diffs (not update_checksums; like each_test_run_and_diff)
971
- # 0 means no diffs (update_checksums; like each_test_run)
972
- @diff = 2
973
- # start with nil. Should only be updated to a non-nil value if test is
974
- # completely successful
975
- @checksum = nil
976
- @re_time = nil # rn_time is in the form of @runtime_seconds
977
-
978
- # these only get used with modern versions of both the sdk and the test
979
- # suite
980
- @rn_mem = nil
981
- @re_mem = nil
982
-
983
- # note: this gets overridden for new runs, so this is probably irrelevant
984
- @summary_text = nil
985
-
986
- # this overrides the submitters choice if it is non-nil
987
- @compiler = mesa.using_sdk ? 'SDK' : nil
988
- # only relevant if @compiler is SDK. Gets set during do_one
989
- @compiler_version = nil
990
-
991
795
  unless MesaTestCase.modules.include? mod
992
796
  raise TestCaseDirError, "Invalid module: #{mod}. Must be one of: " +
993
797
  MesaTestCase.modules.join(', ')
994
798
  end
995
799
  @mod = mod
996
- @failure_msg = {
997
- run_test_string: "#{test_name} run failed: does not match test string",
998
- final_model: "#{test_name} run failed: final model #{final_model} not " \
999
- 'made.',
1000
- run_checksum: "#{test_name} run failed: checksum for #{final_model} " \
1001
- 'does not match after ./rn',
1002
- run_diff: "#{test_name} run failed: diff #{final_model} " \
1003
- 'final_check.mod after ./rn',
1004
- photo_file: "#{test_name} restart failed: #{photo} does not exist",
1005
- photo_checksum: "#{test_name} restart failed: checksum for " \
1006
- "#{final_model} does not match after ./re",
1007
- photo_diff: "#{test_name} restart failed: diff #{final_model} " \
1008
- 'final_check.mod after ./re',
1009
- compilation: "#{test_name} compilation failed"
800
+ @position = position
1010
801
 
1011
- }
1012
- @success_msg = {
1013
- run_test_string: "#{test_name} run: found test string: " \
1014
- "'#{success_string}'",
1015
- run_checksum: "#{test_name} run: checksum for #{final_model} matches " \
1016
- 'after ./rn',
1017
- photo_checksum: "#{test_name} restart: checksum for #{final_model} " \
1018
- "matches after ./re #{photo}"
1019
- }
802
+ # way to output colored text to shell
803
+ @shell = Thor::Shell::Color.new
1020
804
 
1021
805
  # validate stuff
1022
806
  check_mesa_dir
1023
807
  check_test_case
1024
808
 
1025
- @data = {}
1026
- @data_names = []
1027
-
1028
- # way to output colored text to shell
1029
- @shell = Thor::Shell::Color.new
1030
- end
1031
-
1032
- def passed?
1033
- if @outcome == :pass
1034
- true
1035
- elsif @outcome == :fail
1036
- false
1037
- else
1038
- raise TestCaseDirError, 'Cannot determine pass/fail status of ' \
1039
- "#{test_name} yet."
1040
- end
1041
809
  end
1042
810
 
1043
811
  def test_suite_dir
@@ -1048,214 +816,26 @@ class MesaTestCase
1048
816
  File.join(test_suite_dir, test_name)
1049
817
  end
1050
818
 
1051
- def add_datum(datum_name, datum_type)
1052
- unless data_types.include? datum_type.to_sym
1053
- raise InvalidDataType, "Invalid data type: #{datum_type}. Must be one "\
1054
- 'of ' + data_types.join(', ') + '.'
1055
- end
1056
- @data[datum_name] = datum_type
1057
- @data_names << datum_name
1058
- end
1059
-
1060
- def omp_num_threads
1061
- ENV['OMP_NUM_THREADS'].to_i || 1
1062
- end
1063
-
1064
- # based on $MESA_DIR/star/test_suite/each_test_clean, revision 10000
1065
- def clean
1066
- shell.say("Cleaning #{test_name}", color = :yellow)
1067
- # puts ''
1068
- check_mesa_dir
1069
- check_test_case
1070
- in_dir do
1071
- puts './clean'
1072
- unless bash_execute('./clean')
1073
- raise TestCaseDirError, 'Encountered an error while running ./clean ' \
1074
- "in #{Dir.getwd}."
1075
- end
1076
- shell.say 'Removing all files from LOGS, LOGS1, LOGS2, photos, ' \
1077
- 'photos1, and photos2 as well as old test results', color = :blue
1078
- FileUtils.rm_f Dir.glob('LOGS/*')
1079
- FileUtils.rm_f Dir.glob('LOGS1/*')
1080
- FileUtils.rm_f Dir.glob('LOGS2/*')
1081
- FileUtils.rm_f Dir.glob('photos/*')
1082
- FileUtils.rm_f Dir.glob('photos1/*')
1083
- FileUtils.rm_f Dir.glob('photos2/*')
1084
-
1085
- shell.say 'Removing files binary_history.data, out.txt, ' \
1086
- 'test_results.yml, and memory usage files', color = :blue
1087
- FileUtils.rm_f 'binary_history.data'
1088
- FileUtils.rm_f 'out.txt'
1089
- FileUtils.rm_f 'test_results.yml'
1090
- FileUtils.rm_f Dir.glob('mem-*.txt')
1091
- if File.directory? File.join('star_history', 'history_out')
1092
- shell.say 'Removing all files of the form history_out* from ' \
1093
- 'star_history', :blue
1094
- FileUtils.rm_f Dir.glob(File.join('star_history', 'history_out', '*'))
1095
- end
1096
- if File.directory? File.join('star_profile', 'profiles_out')
1097
- shell.say 'Removing all files of the form profiles_out* from ' \
1098
- 'star_profile', color = :blue
1099
- FileUtils.rm_f Dir.glob(File.join('star_profile', 'profiles_out', '*'))
1100
- end
1101
- shell.say 'Removing .running', color = :blue
1102
- FileUtils.rm_f '.running'
1103
- end
1104
- end
1105
-
1106
- # based on $MESA_DIR/star/test_suite/each_test_run_and_diff, revision 10000
819
+ # just punt to +each_test_run+ in the test_suite directory. It's your problem
820
+ # now, sucker!
1107
821
  def do_one
1108
822
  shell.say("Testing #{test_name}", :yellow)
1109
- # puts ''
1110
- test_start = Time.now
1111
- @test_omp_num_threads = omp_num_threads
1112
- if mesa.using_sdk
1113
- version_bin = File.join(ENV['MESASDK_ROOT'], 'bin', 'mesasdk_version')
1114
- # can't use bash_execute because the return value of bash_execute is the
1115
- # exit status of the commmand (true or false), whereas backticks give the
1116
- # output (the version string) as the output
1117
- if File.exist? version_bin
1118
- # newer SDKs have a simple executable
1119
- @compiler_version = `#{version_bin}`.strip
1120
- else
1121
- # older way, call bash on it (file is mesasdk_version.sh)
1122
- @compiler_version = `bash -c #{version_bin + '.sh'}`.strip
1123
- end
1124
-
1125
- shell.say("Using version #{@compiler_version} of the SDK.", :blue)
1126
- end
1127
- in_dir do
1128
- FileUtils.touch '.running'
1129
- build_and_run
1130
- # report memory usage if it is available
1131
- if File.exist?('mem-rn.txt')
1132
- @rn_mem = File.read('mem-rn.txt').strip.to_i
1133
- end
1134
- if File.exist?('mem-re.txt')
1135
- @re_mem = File.read('mem-re.txt').strip.to_i
1136
- end
1137
- FileUtils.rm '.running'
1138
- puts ''
1139
- end
1140
- test_finish = Time.now
1141
- @total_runtime_seconds = (test_finish - test_start).to_i
1142
- end
1143
-
1144
- def log_results
1145
- # gets all parameters that would be submitted as well as computer
1146
- # information and dumps to a yml file in the test case directory
1147
- save_file = File.join(test_case_dir, 'test_results.yml')
1148
- shell.say "Logging test results to #{save_file}...", :blue
1149
- res = {
1150
- 'test_case' => test_name,
1151
- 'module' => mod,
1152
- 'runtime_seconds' => runtime_seconds,
1153
- 're_time' => re_time,
1154
- 'total_runtime_seconds' => total_runtime_seconds,
1155
- 'mesa_version' => mesa_version,
1156
- 'outcome' => outcome,
1157
- 'omp_num_threads' => test_omp_num_threads,
1158
- 'success_type' => success_type,
1159
- 'failure_type' => failure_type,
1160
- 'runtime_minutes' => runtime_minutes,
1161
- 'retries' => retries,
1162
- 'backups' => backups,
1163
- 'steps' => steps,
1164
- 'diff' => diff,
1165
- 'checksum' => checksum,
1166
- 'rn_mem' => rn_mem,
1167
- 're_mem' => re_mem,
1168
- 'summary_text' => summary_text
1169
- }
1170
- if compiler == 'SDK'
1171
- res['compiler'] = 'SDK'
1172
- res['compiler_version'] = compiler_version
1173
- end
1174
- File.open(save_file, 'w') { |f| f.write(YAML.dump(res)) }
1175
- shell.say "Successfully saved results to file #{save_file}.\n", :green
1176
- end
1177
-
1178
- def load_results
1179
- # loads all parameters from a previous test run, likely for submission
1180
- # purposes
1181
- load_file = File.join(test_case_dir, 'test_results.yml')
1182
- shell.say "Loading data from #{load_file}...", :blue
1183
- unless File.exist? load_file
1184
- shell.say "No such file: #{load_file}. No data loaded.", :red
1185
- return
1186
- end
1187
- data = YAML.safe_load(File.read(load_file), [Symbol])
1188
- @runtime_seconds = data['runtime_seconds'] || @runtime_seconds
1189
- @re_time = data['re_time'] || @re_time
1190
- @total_runtime_seconds = data['total_runtime_seconds'] || @total_runtime_seconds
1191
- @mod = data['module'] || @mod
1192
- @mesa_version = data['mesa_version'] || @mesa_version
1193
- @outcome = data['outcome'] || @outcome
1194
- @test_omp_num_threads = data['omp_num_threads'] || @test_omp_num_threads
1195
- @success_type = data['success_type'] || @success_type
1196
- @failure_type = data['failure_type'] || @failure_type
1197
- @runtime_minutes = data['runtime_minutes'] || @runtime_minutes
1198
- @retries = data['retries'] || @retries
1199
- @backups = data['backups'] || @backups
1200
- @steps = data['steps'] || @steps
1201
- @diff = data['diff'] || @diff
1202
- @checksum = data['checksum'] || @checksum
1203
- @rn_mem = data['rn_mem'] || @rn_mem
1204
- @re_mem = data['re_mem'] || @re_mem
1205
- @summary_text = data['summary_text'] || @summary_text
1206
- @compiler = data['compiler'] || @compiler
1207
-
1208
- @compiler_version = data['compiler_version'] || @compiler_version
1209
-
1210
- # convert select data to symbols since that is how they are used
1211
- @outcome = @outcome.to_sym if @outcome
1212
- @success_type = @success_type.to_sym if @success_type
1213
- @failure_type = @failure_type.to_sym if @failure_type
1214
-
1215
- shell.say "Done loading data from #{load_file}.\n", :green
1216
- end
1217
-
1218
- def load_summary_data
1219
- begin
1220
- out_data = parse_out
1221
- @summary_text = get_summary_text
1222
- rescue Errno::ENOENT
1223
- shell.say "\nError loading data from #{out_file}. No summary data "\
1224
- 'loaded. Proceeding anyway.', :red
1225
- else
1226
- @runtime_minutes = out_data[:runtime_minutes]
1227
- @retries = out_data[:retries]
1228
- @backups = out_data[:backups]
1229
- @steps = out_data[:steps]
823
+ visit_dir(test_suite_dir) do
824
+ bash_execute("./each_test_run #{position}")
1230
825
  end
1231
826
  end
1232
827
 
1233
- def parse_out
1234
- runtime_minutes = 0
1235
- retries = 0
1236
- backups = 0
1237
- steps = 0
1238
- run_summaries.each do |summary|
1239
- summary =~ /^\s*runtime\s*\(minutes\),\s+retries,\s+backups,\ssteps\s+(\d+\.?\d*)\s+(\d+)\s+(\d+)\s+(\d+)\s*$/
1240
- runtime_minutes += $1.to_f
1241
- retries += $2.to_i
1242
- backups += $3.to_i
1243
- steps += $4.to_i
828
+ def results_hash
829
+ testhub_file = File.join(test_case_dir, 'testhub.yml')
830
+ unless File.exist?(testhub_file)
831
+ raise TestCaseDirError.new('No results found for test case '\
832
+ "#{test_name}.")
1244
833
  end
1245
- {runtime_minutes: runtime_minutes, retries: retries, backups: backups,
1246
- steps: steps}
834
+ YAML.safe_load(File.read(testhub_file), [Symbol])
1247
835
  end
1248
836
 
1249
837
  private
1250
838
 
1251
- def data_types
1252
- %i[float integer string boolean]
1253
- end
1254
-
1255
- def rn_time
1256
- runtime_seconds
1257
- end
1258
-
1259
839
  # cd into the test case directory, do something in a block, then cd back
1260
840
  # to original directory
1261
841
  def in_dir(&block)
@@ -1265,326 +845,14 @@ class MesaTestCase
1265
845
  # make sure that we can get to the test case directory. Throw an exception
1266
846
  # if we cannot
1267
847
  def check_test_case
1268
- return if File.directory? test_case_dir
848
+ return if dir_or_symlink_exists? test_case_dir
1269
849
  raise TestCaseDirError, "No such test case: #{test_case_dir}."
1270
850
  end
1271
851
 
1272
- # verify that mesa_dir is valid by checking for version number and test_suite
1273
- # directory
852
+ # "verify" that mesa_dir is valid by checking for test_suite directory
1274
853
  def check_mesa_dir
1275
- is_valid = File.exist?(File.join(mesa_dir, 'data', 'version_number')) &&
1276
- File.directory?(test_suite_dir)
1277
- raise MesaDirError, "Invalid MESA dir: #{mesa_dir}" unless is_valid
1278
- end
1279
-
1280
- # append message to log file
1281
- def log_message(msg, color = nil, log_file = 'out.txt')
1282
- if color.nil?
1283
- shell.say(msg)
1284
- else
1285
- shell.say(msg, color)
1286
- end
1287
- File.open(log_file, 'a') { |f| f.puts(msg) }
1288
- end
1289
-
1290
- # write failure message to log file
1291
- def write_failure_message
1292
- msg = "******************** #{failure_msg[@failure_type]} " \
1293
- '********************'
1294
- log_message(msg, :red)
1295
- end
1296
-
1297
- # write success message to log file
1298
- def write_success_msg(success_type)
1299
- msg = 'PASS ' + success_msg[success_type]
1300
- log_message(msg, :green)
1301
- end
1302
-
1303
- # used as return value for run or photo test. Logs failure to text file, and
1304
- # sets internal status to failing
1305
- def fail_test(failure_type)
1306
- @failure_type = failure_type
1307
- @outcome = :fail
1308
- write_failure_message
1309
- false
1310
- end
1311
-
1312
- # used as return value for run or photo test. Logs data to text file, and
1313
- # sets internal status to passing
1314
- def succeed(success_type)
1315
- @success_type = success_type
1316
- @outcome = :pass
1317
- # this should ONLY be read after we are certain we've passed AND that we
1318
- # even have a newly-made checksum
1319
- if File.exist?('checks.md5') && @mesa.update_checksums
1320
- # sometimes we want to ignore the final checksum value
1321
- # we still want to check that restarts on individual machines are bit-for-bit
1322
- # but we don't require bit-for-bit globally, so we report a bogus checksum
1323
- if File.exist?('.ignore_checksum') then
1324
- @checksum ='00000000000000000000000000000000'
1325
- else
1326
- @checksum = File.read('checks.md5').split.first
1327
- end
1328
- end
1329
- write_success_msg(success_type)
1330
- true
1331
- end
1332
-
1333
- def check_run
1334
- # assumes we are in the directory already, called from something else
1335
- run_start = Time.now
1336
-
1337
- # do the run
1338
- rn_command = if ENV['MESASDK_ROOT'] && File.exist?(File.join(ENV['MESASDK_ROOT'], 'bin', 'time'))
1339
- %q(command time -f '%M' -o mem-rn.txt ./rn > out.txt 2> ) +
1340
- 'err.txt'
1341
- else
1342
- './rn >> out.txt 2> err.txt'
1343
- end
1344
- puts rn_command
1345
- bash_execute(rn_command)
1346
-
1347
- # report runtime and clean up
1348
- run_finish = Time.now
1349
- @runtime_seconds = (run_finish - run_start).to_i
1350
- @rn_time = (run_finish - run_start).to_i
1351
- shell.say("Finished with ./rn; runtime = #{@runtime_seconds} seconds.",
1352
- :blue)
1353
- append_and_rm_err
1354
-
1355
- # look for success text
1356
- success = true
1357
- File.open('out.txt', 'r') do |f|
1358
- success = !f.read.downcase.scan(success_string.downcase).empty?
1359
- end
1360
- # bail if there was no test string found
1361
- return fail_test(:run_test_string) unless success
1362
-
1363
- # no final model to check, and we already found the test string, so pass
1364
- return succeed(:run_test_string) unless final_model
1365
-
1366
- # display runtime message
1367
- puts IO.readlines('out.txt').select { |line| line.scan(/runtime/i) }[-1]
1368
-
1369
- # there's supposed to be a final model; check that it exists first
1370
- return fail_test(:final_model) unless File.exist?(final_model)
1371
-
1372
- # update checksums
1373
- #
1374
- # if this is true, behave like each_test_run. update the checksum
1375
- # after rn and then check it matches after re
1376
- #
1377
- # if this is false, behave like each_test_run_and_diff. assume
1378
- # the checksum is up-to-date and check it matches after rn and re.
1379
- if @mesa.update_checksums
1380
- @diff = 0 # this means no diffs run
1381
- puts "md5sum \"#{final_model}\" > checks.md5"
1382
- bash_execute("md5sum \"#{final_model}\" > checks.md5")
1383
- FileUtils.cp final_model, 'final_check.mod'
1384
-
1385
- # if there's no photo, we won't check the checksum, so we've succeeded
1386
- return succeed(:run_test_string) unless photo
1387
- # if there is a photo, we'll have to wait and see
1388
- return true
1389
- end
1390
-
1391
- # check that final model matches
1392
- @diff = 1 # this means diffs were run
1393
- puts './ck >& final_check_diff.txt'
1394
- return fail_test(:run_checksum) unless
1395
- bash_execute('./ck >& final_check_diff.txt')
1396
- return fail_test(:run_diff) if File.exist?('final_check_diff.txt') &&
1397
- !File.read('final_check_diff.txt').empty?
1398
- return succeed(:run_checksum) if File.exist? final_model
1399
- end
1400
-
1401
- # prepare for and do restart, check results, and return pass/fail status
1402
- def check_restart
1403
- # abort if there is not photo specified
1404
- return unless photo
1405
-
1406
- # get penultimate photo
1407
- if photo == "auto" then
1408
- # get all photos [single-star (x100) or binary (b_x100); exclude binary stars (1_x100, 2_x100)]
1409
- photo_files = Dir["photos/*"].select{|p| p =~ /^photos\/(b_)?x?\d+$/}
1410
- # pull out 2nd most recent one
1411
- re_photo = File.basename(photo_files.sort_by { |file_name| File.stat(file_name).mtime } [-2])
1412
- # if binary, trim off prefix
1413
- re_photo.delete_prefix!("b_")
1414
- else
1415
- re_photo = photo
1416
- end
1417
-
1418
- # check that photo file actually exists
1419
- unless File.exist?(File.join('photos', re_photo)) ||
1420
- File.exist?(File.join('photos1', re_photo)) ||
1421
- File.exist?(File.join('photos', "b_#{re_photo}"))
1422
- return fail_test(:photo_file)
1423
- end
1424
-
1425
- # remove final model since it will be remade by restart
1426
- FileUtils.rm_f final_model
1427
-
1428
- # do restart and consolidate output. Command depends on if we have access
1429
- # to SDK version of gnu time.
1430
- re_command = if ENV['MESASDK_ROOT'] && File.exist?(File.join(ENV['MESASDK_ROOT'], 'bin', 'time'))
1431
- %q(command time -f '%M' -o mem-re.txt ./re ) + "#{re_photo}" \
1432
- ' >> out.txt 2> err.txt'
1433
- else
1434
- "./re #{re_photo} >> out.txt 2> err.txt"
1435
- end
1436
-
1437
- puts re_command
1438
- # puts "./re #{re_photo} >> out.txt 2> err.txt"
1439
- re_start = Time.now
1440
- # bash_execute("./re #{re_photo} >> out.txt 2> err.txt")
1441
- # bash_execute(%Q{command time -f '%M' -o mem-re.txt ./re #{re_photo} >> out.txt 2> err.txt})
1442
- bash_execute(re_command)
1443
- re_finish = Time.now
1444
- @re_time = (re_finish - re_start).to_i
1445
- append_and_rm_err
1446
-
1447
- # check that final model matches
1448
- puts './ck >& final_check_diff.txt'
1449
- return fail_test(:photo_checksum) unless
1450
- bash_execute('./ck >& final_check_diff.txt')
1451
- return fail_test(:photo_diff) if
1452
- File.exist?('final_check_diff.txt') &&
1453
- !File.read('final_check_diff.txt').empty?
1454
- succeed(:photo_checksum)
1455
- end
1456
-
1457
- def build_and_run
1458
- # assumes we are in the test case directory. Should only be called
1459
- # in the context of an `in_dir` block.
1460
-
1461
- # first clean and make... Should be compatible with any shell since
1462
- # redirection is always wrapped in 'bash -c "{STUFF}"'
1463
- simple_clean
1464
- begin
1465
- mk
1466
- rescue TestCaseDirError
1467
- return fail_test(:compilation)
1468
- end
1469
-
1470
- # remove old final model if it exists
1471
- remove_final_model
1472
-
1473
- # only check restart/photo if we get through run successfully
1474
- check_restart if check_run
1475
-
1476
- # get reported runtime, retries, backups, and steps
1477
- load_summary_data if File.exist?(out_file)
1478
- end
1479
-
1480
- # append contents of err.txt to end of out.txt, then delete err.txt
1481
- def append_and_rm_err(outfile = 'out.txt', errfile = 'err.txt')
1482
- err_contents = File.read(errfile)
1483
- display_errors(err_contents)
1484
- log_errors(err_contents, outfile)
1485
- FileUtils.rm errfile
1486
- end
1487
-
1488
- def display_errors(err_contents)
1489
- return if err_contents.strip.empty?
1490
- shell.say("\nERRORS", :red)
1491
- puts err_contents
1492
- shell.say('END OF ERRORS', :red)
1493
- end
1494
-
1495
- def log_errors(err_contents, outfile)
1496
- return if err_contents.strip.empty?
1497
- File.open(outfile, 'a') { |f_out| f_out.write(err_contents) }
1498
- shell.say("appended to #{outfile}\n", :red)
1499
- end
1500
-
1501
- def simple_clean
1502
- puts './clean'
1503
- return if bash_execute('./clean')
1504
- raise TestCaseDirError, 'Encountered an error when running `clean` in ' \
1505
- "#{Dir.getwd} for test case #{test_name}."
1506
- end
1507
-
1508
- def mk
1509
- puts './mk > mk.txt'
1510
- unless bash_execute('./mk > mk.txt')
1511
- raise TestCaseDirError, 'Encountered an error when running `mk` in ' \
1512
- "#{Dir.getwd} for test case #{test_name}."
1513
- end
1514
- FileUtils.rm 'mk.txt'
1515
- end
1516
-
1517
- def remove_final_model
1518
- # remove final model if it already exists
1519
- return unless final_model
1520
- return unless File.exist?(final_model)
1521
- FileUtils.rm(final_model)
1522
- end
1523
-
1524
- def out_file
1525
- File.join(test_case_dir, 'out.txt')
1526
- end
1527
-
1528
- # helpers for getting run summaries
1529
- def run_summaries
1530
- # look at all lines in out.txt
1531
- lines = IO.readlines(out_file)
1532
-
1533
- # find lines with summary information
1534
- summary_line_numbers = []
1535
- lines.each_with_index do |line, i|
1536
- if line =~ /^\s*runtime \(minutes\),\s+retries,\s+backups,\ssteps/
1537
- summary_line_numbers << i
1538
- end
1539
- end
1540
-
1541
- # find lines indicating passage or failure of runs and restarts
1542
- run_finish_line_numbers = []
1543
- restart_finish_line_numbers = []
1544
- lines.each_with_index do |line, i|
1545
- if line =~ /^\s*((?:PASS)|(?:FAIL))\s+#{test_name}\s+restart/
1546
- restart_finish_line_numbers << i
1547
- elsif line =~ /^\s*((?:PASS)|(?:FAIL))\s+#{test_name}\s+run/
1548
- run_finish_line_numbers << i
1549
- end
1550
- end
1551
-
1552
- # only keep summaries that correspond to runs rather than restart
1553
- summary_line_numbers.select do |i|
1554
- run_summary?(i, run_finish_line_numbers, restart_finish_line_numbers)
1555
- end.map { |line_number| lines[line_number] }
1556
- end
1557
-
1558
- def get_summary_text
1559
- # original plan was to include diff data in summary text... now it's just
1560
- # part of the test_instance object and is submitted as an integer
1561
- # res = case diff
1562
- # when 0
1563
- # "No Diff\n"
1564
- # when 1
1565
- # "Diff\n"
1566
- # else
1567
- # "Ambiguous Diff\n"
1568
- # end
1569
- # res +
1570
- IO.readlines(out_file).select do |line|
1571
- line =~ /^\s*runtime/
1572
- end.join
1573
- end
1574
-
1575
- def run_summary?(i, run_finish_line_numbers, restart_finish_line_numbers)
1576
- # iterate from starting line (a summary line) up to largest PASS/FAIL
1577
- # line, bail out if summary line is beyond any PASS/FAIL line
1578
- max_line = run_finish_line_numbers.max || 0
1579
- max_line = [max_line, (restart_finish_line_numbers.max || 0)].max
1580
- return false if i > max_line
1581
- # return true if next PASS/FAIL line is for a run and fail if it is for a
1582
- # restart
1583
- i.upto(max_line) do |j|
1584
- return true if run_finish_line_numbers.include?(j)
1585
- return false if restart_finish_line_numbers.include?(j)
1586
- end
1587
- false
854
+ is_valid = dir_or_symlink_exists? test_suite_dir
855
+ raise MesaDirError, "Invalid MESA dir: #{mesa.mesa_dir}" unless is_valid
1588
856
  end
1589
857
 
1590
858
  end
@@ -1599,13 +867,11 @@ end
1599
867
  def visit_and_check(new_dir, exception, message)
1600
868
  cwd = Dir.getwd
1601
869
  shell.say "Leaving #{cwd}", :blue
1602
- puts ''
1603
- shell.say "Entering #{new_dir}.", :blue
870
+ shell.say "\nEntering #{new_dir}.", :blue
1604
871
  Dir.chdir(new_dir)
1605
872
  success = yield if block_given?
1606
873
  shell.say "Leaving #{new_dir}", :blue
1607
- puts ''
1608
- shell.say "Entering #{cwd}.", :blue
874
+ shell.say "\nEntering #{cwd}.", :blue
1609
875
  Dir.chdir(cwd)
1610
876
  return if success
1611
877
  raise exception, message
@@ -1613,18 +879,18 @@ end
1613
879
 
1614
880
  # cd into a new directory, execute a block, then cd back into original
1615
881
  # directory
1616
- def visit_dir(new_dir)
882
+ def visit_dir(new_dir, quiet: false)
1617
883
  cwd = Dir.getwd
1618
- shell.say "Leaving #{cwd}\n", :blue
1619
- shell.say "Entering #{new_dir}.", :blue
884
+ shell.say "Leaving #{cwd}\n", :blue unless quiet
885
+ shell.say "\nEntering #{new_dir}.", :blue unless quiet
1620
886
  Dir.chdir(new_dir)
1621
887
  yield if block_given?
1622
- shell.say "Leaving #{new_dir}\n", :blue
1623
- shell.say "Re-entering #{cwd}.", :blue
1624
- puts ""
888
+ shell.say "Leaving #{new_dir}\n", :blue unless quiet
889
+ shell.say "\nRe-entering #{cwd}.", :blue unless quiet
1625
890
  Dir.chdir(cwd)
1626
891
  end
1627
892
 
893
+ # the next function probalby doesn't belong here, but keep it anyway, please
1628
894
  # create seed data for test cases for MesaTestHub of a given mesa version
1629
895
  def generate_seeds_rb(mesa_dir, outfile)
1630
896
  m = Mesa.new(mesa_dir: mesa_dir)
@@ -1635,7 +901,6 @@ def generate_seeds_rb(mesa_dir, outfile)
1635
901
  m.test_names.each do |test_case_name|
1636
902
  f.puts ' {'
1637
903
  f.puts " name: '#{test_case_name}',"
1638
- f.puts " version_added: #{m.version_number},"
1639
904
  # no comma on last one
1640
905
  if test_case_name == m.test_names[-1]
1641
906
  f.puts(' }')
@@ -1648,7 +913,22 @@ def generate_seeds_rb(mesa_dir, outfile)
1648
913
  end
1649
914
  end
1650
915
 
916
+ def dir_or_symlink_exists?(path)
917
+ File.directory?(path) || File.symlink?(path)
918
+ end
919
+
1651
920
  # force the execution to happen with bash
1652
- def bash_execute(command)
1653
- system('bash -c "' + command + '"')
921
+ def bash_execute(command, throw_exception=false)
922
+ res = system('bash -c "' + command + '"')
923
+ if !res && throw_exception
924
+ raise BashError('Encountered an error when executing the following '\
925
+ "command in bash: #{command}.")
926
+ end
927
+ res
928
+ end
929
+
930
+ # force execution to happen with bash, but return result rather than exit
931
+ # status (like backticks)
932
+ def bashticks(command)
933
+ `bash -c "#{command}"`.chomp
1654
934
  end