mesa_test 0.2.15 → 1.0.4

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 +460 -1172
  4. metadata +6 -7
@@ -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,59 +48,54 @@ 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
- # Confirm save location
89
- response = shell.ask "This will be saved in #{s.config_file}. Press " \
90
- 'enter to accept or enter a new location:', :blue, path: true
91
- s.config_file = response unless response.empty?
78
+ # we are powerless to do change the location for now, so stop asking
79
+ # about it
80
+ # # Confirm save location
81
+ # response = shell.ask "This will be saved in #{s.config_file}. Press " \
82
+ # 'enter to accept or enter a new location:', :blue, path: true
83
+ # s.config_file = response unless response.empty?
92
84
  end
93
85
 
94
86
  # Confirm data. If not confirmed, restart whole wizard.
95
87
  if confirm_computer_data
96
88
  save_computer_data
97
89
  else
98
- puts "Restarting wizard.\n"
90
+ shell.say "Restarting wizard.\n"
99
91
  setup
100
92
  end
101
93
  end
102
94
 
103
95
  def self.new_from_config(
104
- config_file: File.join(ENV['HOME'], '.mesa_test.yml'), force_setup: false,
96
+ config_file: File.join(ENV['HOME'], '.mesa_test', 'config.yml'),
97
+ force_setup: false,
105
98
  base_uri: DEFAULT_URI
106
- # base_uri: 'http://localhost:3000'
107
99
  )
108
100
  new_submitter = new(config_file: config_file, base_uri: base_uri)
109
101
  if force_setup
@@ -117,22 +109,27 @@ e-mail and password will be stored in plain text.'
117
109
  end
118
110
 
119
111
  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
112
+ :mesa_mirror, :mesa_work, :platform_version, :processor,
113
+ :config_file, :base_uri, :last_tested, :github_protocol
122
114
 
123
115
  attr_reader :shell
124
116
 
125
117
  # many defaults are set in body
126
118
  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
119
+ computer_name: nil, user_name: nil, email: nil, github_protocol: nil,
120
+ mesa_mirror: nil, platform: nil, platform_version: nil, processor: nil,
121
+ config_file: nil, base_uri: nil, last_tested: nil
130
122
  )
131
123
  @computer_name = computer_name || Socket.gethostname.scan(/^[^\.]+\.?/)[0]
132
124
  @computer_name.chomp!('.') if @computer_name
133
125
  @user_name = user_name || (ENV['USER'] || ENV['USERNAME'])
134
126
  @email = email || ''
135
127
  @password = password || ''
128
+ @github_protocol = github_protocol || :ssh
129
+ @mesa_mirror = mesa_mirror ||
130
+ File.join(ENV['HOME'], '.mesa_test', 'mirror')
131
+ @mesa_work = mesa_work ||
132
+ File.join(ENV['HOME'], '.mesa_test', 'work')
136
133
  @platform = platform
137
134
  if @platform.nil?
138
135
  @platform =
@@ -146,17 +143,15 @@ e-mail and password will be stored in plain text.'
146
143
  end
147
144
  @platform_version = platform_version || ''
148
145
  @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')
146
+ @config_file = config_file || File.join(ENV['HOME'], '.mesa_test',
147
+ 'config.yml')
153
148
  @base_uri = base_uri
154
- @last_tested = last_tested || DEFAULT_REVISION
155
149
 
156
150
  # set up thor-proof way to get responses from user. Thor hijacks the
157
151
  # gets command, so we have to use its built-in "ask" method, which is
158
152
  # actually more useful
159
153
  @shell = Thor::Shell::Color.new
154
+
160
155
  yield self if block_given?
161
156
  end
162
157
 
@@ -169,15 +164,13 @@ e-mail and password will be stored in plain text.'
169
164
  puts 'Ready to submit the following data:'
170
165
  puts '-------------------------------------------------------'
171
166
  puts "Computer Name #{computer_name}"
172
- puts "User Name #{user_name}"
173
167
  puts "User email #{email}"
174
168
  puts 'Password ***********'
169
+ puts "GitHub Protocol #{github_protocol}"
170
+ puts "MESA Mirror Location #{mesa_mirror}"
171
+ puts "MESA Work Location #{mesa_work}"
175
172
  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
- puts "Config location #{config_file}"
173
+ # puts "Config location #{config_file}"
181
174
  puts '-------------------------------------------------------'
182
175
  puts ''
183
176
  response = shell.ask 'Is this correct? (y/Y = Yes, anything else = No):'
@@ -189,158 +182,114 @@ e-mail and password will be stored in plain text.'
189
182
  # change platforms (i.e. switch from mac to linux, or change between linux
190
183
  # flavors), you should create a new computer account. Similarly, create new
191
184
  # 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.
185
+ # to change computers if you upgrade your platform (macOS 10.12 -> 10.13
201
186
  def save_computer_data
202
187
  data_hash = {
203
188
  'computer_name' => computer_name,
204
- 'user_name' => user_name,
205
189
  'email' => email,
206
190
  'password' => password,
191
+ 'github_protocol' => github_protocol,
192
+ 'mesa_mirror' => mesa_mirror,
193
+ 'mesa_work' => mesa_work,
207
194
  'platform' => platform,
208
- 'processor' => processor,
209
- 'ram_gb' => ram_gb,
210
195
  'platform_version' => platform_version,
211
- 'compiler' => compiler,
212
- 'compiler_version' => compiler_version,
213
- 'last_tested' => last_tested
214
196
  }
197
+ # make sure there's a directory to write to
198
+ unless dir_or_symlink_exists? File.dirname(config_file)
199
+ FileUtils.mkdir_p File.dirname(config_file)
200
+ end
215
201
  File.open(config_file, 'w') { |f| f.write(YAML.dump(data_hash)) }
216
202
  end
217
203
 
218
204
  def load_computer_data
219
205
  data_hash = YAML.safe_load(File.read(config_file), [Symbol])
220
206
  @computer_name = data_hash['computer_name']
221
- @user_name = data_hash['user_name']
222
207
  @email = data_hash['email']
223
208
  @password = data_hash['password']
209
+ @github_protocol = data_hash['github_protocol'].to_sym
210
+ @mesa_mirror = data_hash['mesa_mirror']
211
+ @mesa_work = data_hash['mesa_work']
224
212
  @platform = data_hash['platform']
225
- @processor = data_hash['processor']
226
- @ram_gb = data_hash['ram_gb']
227
213
  @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
214
  end
232
215
 
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,
216
+ # Parameters to be submitted in JSON format for reporting information about
217
+ # the submitting user and computer
218
+ def submitter_params
219
+ {
241
220
  email: email,
242
221
  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
222
+ computer: computer_name,
223
+ platform_version: platform_version
262
224
  }
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
225
  end
272
226
 
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
227
+ # Parameters to be submitted in JSON format for reporting information about
228
+ # the overall commit being tested; used even if only submitting an entire
229
+ # test. This also determines if the submission is for an entire commit
230
+ # (compilation information and every test), an empty commit (just
231
+ # compilation information), or a non-empty, but also non-entire submission
232
+ # (results for a single test without compilation information)
233
+ def commit_params(mesa, entire: true, empty: false)
234
+ # the compiler data should be able to be used as-is, but right now the
235
+ # names don't match with what the database expects, so we do some renaming
236
+ # shenanigans.
237
+ #
238
+ ####################################
239
+ # THIS SHOULD GO BEFORE PRODUCTION #
240
+ {
241
+ sha: mesa.sha,
242
+ compiled: mesa.installed?,
243
+ entire: entire,
244
+ empty: empty,
245
+ }.merge(mesa.compiler_hash)
246
+ end
247
+
248
+ # Given a valid +Mesa+ object, create an array of hashes that describe the
249
+ # test cases and the test results. These will be encoded as an array of
250
+ # JSON objects.
251
+ def instance_params(mesa)
296
252
  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|
253
+ res = []
254
+ mesa.test_case_names.each do |mod, names|
301
255
  names.each do |test_name|
302
256
  begin
303
257
  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
- }
258
+ res << test_case.results_hash
327
259
  rescue TestCaseDirError
328
- shell.say "Passage status for #{test_case.test_name} not yet "\
329
- 'known. Run test first and then submit.', :red
260
+ # shell.say "It appears that #{test_case.test_name} has not been "\
261
+ # 'run yet. Unable to submit data for this test.', :red
330
262
  has_errors << test_case
331
263
  end
332
264
  end
333
265
  end
334
- [res, has_errors]
266
+ unless has_errors.empty?
267
+ shell.say "The following test cases could NOT be read for submission:",
268
+ :red
269
+ has_errors.each do |test_case|
270
+ shell.say "- #{test_case.test_name}", :red
271
+ end
272
+ end
273
+ res
274
+ end
275
+
276
+ # Parameters for a single test case. +mesa+ is an instance of +Mesa+, and
277
+ # +test_case+ is an instance of MesaTestCase representing the test case to
278
+ # be submitted
279
+ def single_instance_params(test_case)
280
+ [test_case.results_hash]
335
281
  end
336
282
 
283
+ # Phone home to testhub and confirm that computer and user are valid. Useful
284
+ # for confirming that submissions will be accepted before wasting time on a
285
+ # test later.
337
286
  def confirm_computer
338
287
  uri = URI.parse(base_uri + '/check_computer.json')
339
288
  https = Net::HTTP.new(uri.hostname, uri.port)
340
289
  https.use_ssl = base_uri.include? 'https'
341
290
 
342
291
  request = Net::HTTP::Post.new(
343
- uri, initheader = { 'Content-Type' => 'application/json' }
292
+ uri, initheader = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }
344
293
  )
345
294
  request.body = {
346
295
  email: email,
@@ -350,295 +299,241 @@ e-mail and password will be stored in plain text.'
350
299
  JSON.parse(https.request(request).body).to_hash
351
300
  end
352
301
 
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')
302
+ # submit entire commit's worth of test cases, OR submit compilation status
303
+ # and NO test cases
304
+ def submit_commit(mesa, empty: false)
305
+ unless mesa.install_attempted?
306
+ raise MesaDirError, 'No testhub.yml file found in installation; '\
307
+ 'must attempt to install before subitting.'
308
+ end
309
+ uri = URI.parse(base_uri + '/submissions/create.json')
359
310
  https = Net::HTTP.new(uri.hostname, uri.port)
360
311
  https.use_ssl = true if base_uri.include? 'https'
361
312
 
362
313
  request = Net::HTTP::Post.new(
363
314
  uri,
364
- initheader = { 'Content-Type' => 'application/json' }
315
+ initheader = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }
365
316
  )
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
317
 
374
- # verbose = true
375
- # puts "\n" if verbose
376
- # puts JSON.parse(request.body).to_hash if verbose
318
+ # create the request body for submission to the submissions API
319
+ #
320
+ # if we have an empty submission, then it is necessarily not entire.
321
+ # Similarly, a non-empty submission is necessarily entire (otherwise one
322
+ # would use +submit_instance+)
323
+ request_data = {submitter: submitter_params,
324
+ commit: commit_params(mesa, empty: empty, entire: !empty)}
325
+ # don't need test instances if it's an empty submission or if compilation
326
+ # failed
327
+ if !empty && request_data[:commit][:compiled]
328
+ request_data[:instances] = instance_params(mesa)
329
+ end
330
+ request.body = request_data.to_json
377
331
 
332
+ # actually do the submission
378
333
  response = https.request request
379
- # puts JSON.parse(response.body).to_hash if verbose
380
- response.is_a? Net::HTTPCreated
381
- end
382
334
 
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
335
+ if !response.is_a? Net::HTTPCreated
336
+ shell.say "\nFailed to submit some or all test case instances and/or "\
337
+ 'commit data.', :red
338
+ false
391
339
  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
340
+ shell.say "\nSuccessfully submitted commit #{mesa.sha}.", :green
341
+ true
424
342
  end
425
- # return boolean indicating whether or not all cases successfully
426
- # SUBMITTED (irrespective of passing status)
427
- success
428
343
  end
429
344
 
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')
345
+ # submit results for a single test case instance. Does *not* report overall
346
+ # compilation status to testhub. Use an empty commit submission for that
347
+ def submit_instance(mesa, test_case)
348
+ unless mesa.install_attempted?
349
+ raise MesaDirError, 'No testhub.yml file found in installation; '\
350
+ 'must attempt to install before subitting.'
351
+ end
352
+
353
+ uri = URI.parse(base_uri + '/submissions/create.json')
434
354
  https = Net::HTTP.new(uri.hostname, uri.port)
435
355
  https.use_ssl = true if base_uri.include? 'https'
436
356
 
437
357
  request = Net::HTTP::Post.new(
438
358
  uri,
439
- initheader = { 'Content-Type' => 'application/json' }
359
+ initheader = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }
440
360
  )
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
361
 
449
- # verbose = true
450
- # puts "\n" if verbose
451
- # puts JSON.parse(request.body).to_hash if verbose
362
+ # create the request body for submission to the submissions API
363
+ #
364
+ # submission is not empty (there is one test case), and it is also not
365
+ # entire (... there is only test case)
366
+ request_data = {submitter: submitter_params,
367
+ commit: commit_params(mesa, empty: false, entire: false),
368
+ instances: single_instance_params(test_case)}
369
+ request.body = request_data.to_json
452
370
 
371
+ # actually do the submission
453
372
  response = https.request request
454
- # puts JSON.parse(response.body).to_hash if verbose
373
+
455
374
  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 }
375
+ shell.say "\nFailed to submit #{test_case.test_name} for commit "\
376
+ "#{mesa.sha}", :red
462
377
  false
463
378
  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
379
+ shell.say "\nSuccessfully submitted instance of #{test_case.test_name} "\
380
+ "for commit #{mesa.sha}.", :green
381
+ true
469
382
  end
470
383
  end
471
384
  end
472
385
 
473
386
  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
387
+ attr_reader :mesa_dir, :mirror_dir, :names_to_numbers, :shell,
388
+ :test_case_names, :test_cases, :github_protocol
532
389
 
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
390
+ def self.checkout(sha: nil, work_dir: nil, mirror_dir: nil,
391
+ github_protocol: :ssh)
392
+ m = Mesa.new(mesa_dir: work_dir, mirror_dir: mirror_dir,
393
+ github_protocol: github_protocol)
394
+ m.checkout(new_sha: sha)
395
+ m
539
396
  end
540
397
 
541
- def initialize(mesa_dir: ENV['MESA_DIR'], use_svn: true, using_sdk: true)
398
+ def initialize(mesa_dir: ENV['MESA_DIR'], mirror_dir: nil,
399
+ github_protocol: :ssh)
542
400
  # absolute_path ensures that it doesn't matter where commands are executed
543
401
  # from
544
402
  @mesa_dir = File.absolute_path(mesa_dir)
545
- @use_svn = use_svn
546
- @using_sdk = using_sdk
547
- @update_checksums = false
403
+ @mirror_dir = File.absolute_path(mirror_dir)
404
+
405
+ # don't worry about validity of github protocol until it is needed in a
406
+ # checkout. This way you can have garbage in there if you never really need
407
+ # it.
408
+ @github_protocol = if github_protocol.respond_to? :to_sym
409
+ github_protocol.to_sym
410
+ else
411
+ github_protocol
412
+ end
548
413
 
549
414
  # these get populated by calling #load_test_data
550
- @test_data = {}
551
- @test_names = {}
552
415
  @test_cases = {}
416
+ @test_case_names = {}
417
+ @names_to_numbers = {}
553
418
 
554
419
  # way to output colored text
555
420
  @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
421
  end
563
422
 
564
- def use_svn?
565
- @use_svn
566
- end
567
-
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."
423
+ def checkout(new_sha: 'HEAD')
424
+ # before anything confirm that git-lfs has been installed
425
+ shell.say "\nEnsuring that git-lfs is installed... ", :blue
426
+ command = 'git lfs help >> /dev/null 2>&1'
427
+ if bash_execute(command)
428
+ shell.say "yes", :green
587
429
  else
588
- shell.say "From svn log, automatically decided to take diffs."
430
+ shell.say "no", :red
431
+ raise(GitHubError, "The command #{command} returned with an error "\
432
+ 'status, indicating that git-lfs is not installed. '\
433
+ 'Make sure it is installed and try again.')
434
+ end
435
+
436
+ # set up mirror if it doesn't exist
437
+ unless dir_or_symlink_exists?(mirror_dir)
438
+ shell.say "\nCreating initial mirror at #{mirror_dir}. "\
439
+ 'This might take awhile...', :blue
440
+ FileUtils.mkdir_p mirror_dir
441
+ case github_protocol
442
+ when :ssh
443
+ command = "git clone --mirror #{GITHUB_SSH} #{mirror_dir}"
444
+ shell.say command
445
+ # fail loudly if this doesn't work
446
+ unless bash_execute(command)
447
+ # nuke the mirror directory since it is probably bogus (make this
448
+ # code fire off the next time checkout is done)
449
+ shell.say "Failed. Removing the [potentially corrupted] mirror.", :red
450
+ command = "rm -rf #{mirror_dir}"
451
+ shell.say command
452
+ bash_execute(command)
453
+
454
+ raise(GitHubError, 'Error while executing the following command:'\
455
+ "#{command}. Perhaps you haven't set up "\
456
+ 'ssh keys with your GitHub account?')
457
+ end
458
+ when :https
459
+ command = "git clone --mirror #{GITHUB_HTTPS} #{mirror_dir}"
460
+ shell.say command
461
+ # fail loudly if this doesn't work
462
+ unless bash_execute(command)
463
+ # nuke the mirror directory since it is probably bogus (make this
464
+ # code fire off the next time checkout is done)
465
+ shell.say "Failed. Removing the [potentially corrupted] mirror.", :red
466
+ command = "rm -rf #{mirror_dir}"
467
+ shell.say command
468
+ bash_execute(command)
469
+
470
+ raise(GitHubError, 'Error while executing the following command: '\
471
+ "#{command}. Perhaps you need to configure "\
472
+ 'global GitHub account settings for https '\
473
+ 'authentication to work properly?')
474
+ end
475
+ else
476
+ raise(GitHubError, "Invalid GitHub protocol: \"#{github_protocol}\"")
477
+ end
589
478
  end
590
- shell.say "log entry: #{@svn_log}"
591
- end
592
479
 
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
480
+ update_mirror
481
+
482
+ # ensure "work" directory is removed from worktree
483
+ remove
484
+
485
+ # create "work" directory with proper commit
486
+ shell.say "\nSetting up worktree repo...", :blue
487
+ FileUtils.mkdir_p mesa_dir
488
+ command = "git -C #{mirror_dir} worktree add #{mesa_dir} #{new_sha}"
489
+ shell.say command
490
+ return if bash_execute(command)
491
+
492
+ raise(GitHubError, 'Failed while executing the following command: '\
493
+ "\"#{command}\".")
598
494
  end
599
495
 
600
- def log_entry
601
- `svn log #{mesa_dir} -r #{version_number}`
496
+ def update_mirror
497
+ shell.say "\nFetching MESA history...", :blue
498
+ command = "git -C #{mirror_dir} fetch origin"
499
+ shell.say command
500
+ # fail loudly
501
+ return if bash_execute(command)
502
+
503
+ raise(GitHubError, 'Failed while executing the following command: '\
504
+ "\"#{command}\".")
602
505
  end
603
506
 
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
507
+ def remove
508
+ return unless File.exist? mesa_dir
509
+ shell.say "\nRemoving work directory from worktree (clearing old data)...",
510
+ :blue
511
+ command = "git -C #{mirror_dir} worktree remove --force #{mesa_dir}"
512
+ shell.say command
513
+ return if bash_execute(command)
514
+
515
+ shell.say "Failed. Simply trying to remove the directory.", :red
516
+ command = "rm -rf #{mesa_dir}"
517
+ shell.say command
518
+ # fail loudly (the "true" tells bash_execute to raise an exception if
519
+ # the command fails)
520
+ bash_execute(command, true)
612
521
  end
613
522
 
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
523
+ def git_sha
524
+ bashticks("git -C #{mesa_dir} rev-parse HEAD")
625
525
  end
626
526
 
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
527
+ def sha
528
+ git_sha
634
529
  end
635
530
 
636
531
  def clean
637
532
  with_mesa_dir do
638
533
  visit_and_check mesa_dir, MesaDirError, 'E\countered a problem in ' \
639
534
  "running `clean` in #{mesa_dir}." do
640
- puts 'MESA_DIR = ' + ENV['MESA_DIR']
641
- puts './clean'
535
+ shell.say('MESA_DIR = ' + ENV['MESA_DIR'])
536
+ shell.say './clean'
642
537
  bash_execute('./clean')
643
538
  end
644
539
  end
@@ -649,8 +544,8 @@ class Mesa
649
544
  with_mesa_dir do
650
545
  visit_and_check mesa_dir, MesaDirError, 'Encountered a problem in ' \
651
546
  "running `install` in #{mesa_dir}." do
652
- puts 'MESA_DIR = ' + ENV['MESA_DIR']
653
- puts './install'
547
+ shell.say('MESA_DIR = ' + ENV['MESA_DIR'])
548
+ shell.say './install'
654
549
  bash_execute('./install')
655
550
  end
656
551
  end
@@ -661,20 +556,34 @@ class Mesa
661
556
 
662
557
  # throw an error unless it seems like it's properly compiled
663
558
  def check_installation
664
- unless installed?
665
- raise MesaDirError, 'Installation check failed (no .mod files found ' \
666
- 'in the last compiled module).'
667
- end
668
- end
559
+ return if installed?
669
560
 
670
- def destroy
671
- FileUtils.rm_rf mesa_dir
561
+ raise MesaDirError, 'Installation check failed (build.log doesn\'t '\
562
+ 'show a successful installation).'
563
+ end
564
+
565
+ # sourced from $MESA_DIR/testhub.yml, which should be created after
566
+ # installation
567
+ def compiler_hash
568
+ data_file = File.join(mesa_dir, 'testhub.yml')
569
+ res = {
570
+ compiler: 'Unknown',
571
+ sdk_version: 'Unknown',
572
+ math_backend: 'Unknown'
573
+ }
574
+ if File.exist? data_file
575
+ res = res.merge(YAML.safe_load(File.read(data_file)) || {})
576
+ # currently version_number is reported, but we don't need that in Git land
577
+ res.delete('version_number') # returns the value, not the updated hash
578
+ res
579
+ end
672
580
  end
673
581
 
674
582
  ## TEST SUITE METHODS
675
583
 
676
584
  def check_mod(mod)
677
585
  return if MesaTestCase.modules.include? mod
586
+
678
587
  raise TestCaseDirError, "Invalid module: #{mod}. Must be one of: " +
679
588
  MesaTestCase.modules.join(', ')
680
589
  end
@@ -694,53 +603,32 @@ class Mesa
694
603
  end
695
604
  else
696
605
  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
606
 
702
- # initialize data hash to empty hash and name array to empty array
703
- @test_data[mod] = {}
704
- @test_names[mod] = []
607
+ # convert output of +list_tests+ to a dictionary that maps
608
+ # names to numbers since +each_test_run+ only knows about numbers
609
+ @names_to_numbers[mod] = {}
610
+ @test_case_names[mod] = []
705
611
  @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
612
+ visit_dir(test_suite_dir(mod: mod), quiet: true) do
613
+ bashticks('./list_tests').split("\n").each do |line|
614
+ num, tc_name = line.strip.split
615
+ @names_to_numbers[mod][tc_name.strip] = num.to_i
616
+ @test_case_names[mod] << tc_name.strip
617
+ @test_cases[mod][tc_name.strip] = MesaTestCase.new(
618
+ test: tc_name.strip,
619
+ mod: mod,
620
+ position: num.to_i,
621
+ mesa: self
622
+ )
730
623
  end
731
624
  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
625
  end
742
626
  end
743
627
 
628
+ def test_case_count(mod: :all)
629
+ all_names_ordered(mod: mod).count
630
+ end
631
+
744
632
  # can accept a number (in string form) as a name for indexed access
745
633
  def find_test_case(test_case_name: nil, mod: :all)
746
634
  if /\A[0-9]+\z/ =~ test_case_name
@@ -750,44 +638,16 @@ class Mesa
750
638
  end
751
639
  end
752
640
 
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)
641
+ def each_test_run(mod: :all)
767
642
  check_installation
768
- each_test_clean(mod: mod)
769
643
 
770
644
  if mod == :all
771
645
  MesaTestCase.modules.each do |this_mod|
772
- each_test_run_and_diff(mod: this_mod, log_results: log_results)
646
+ each_test_run(mod: this_mod)
773
647
  end
774
648
  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
-
783
- def each_test_load_results(mod: :all)
784
- if mod == :all
785
- MesaTestCase.modules.each do |this_mod|
786
- each_test_load_results(mod: this_mod)
787
- end
788
- else
789
- test_names[mod].each do |test_name|
790
- test_cases[mod][test_name].load_results
649
+ visit_dir(test_suite_dir(mod: mod)) do
650
+ bash_execute('./each_test_run')
791
651
  end
792
652
  end
793
653
  end
@@ -797,34 +657,26 @@ class Mesa
797
657
  end
798
658
 
799
659
  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?
660
+ # assume build log reflects installation status; does not account for
661
+ # mucking with modules after the fact
662
+ build_log = File.join(mesa_dir, 'build.log')
663
+ downloaded? && File.exist?(build_log) && File.read(build_log).include?(
664
+ 'MESA installation was successful'
665
+ )
815
666
  end
816
667
 
668
+ def install_attempted?
669
+ File.exist? File.join(mesa_dir, 'testhub.yml')
670
+ end
817
671
 
818
672
  private
819
673
 
820
- # verify that mesa_dir is valid by checking for version number and test_suite
821
- # directory
674
+ # verify that mesa_dir is valid by checking for existence of test_suite
675
+ # directory for each module (somewhat arbitrary)
822
676
  def check_mesa_dir
823
- res = File.exist?(File.join(mesa_dir, 'data', 'version_number'))
824
- MesaTestCase.modules.each do |mod|
825
- res &&= dir_or_symlink_exists?(test_suite_dir(mod: mod))
677
+ MesaTestCase.modules.inject(true) do |res, mod|
678
+ res && dir_or_symlink_exists?(test_suite_dir(mod: mod))
826
679
  end
827
- res
828
680
  end
829
681
 
830
682
  # change MESA_DIR for the execution of the block and then revert to the
@@ -845,49 +697,64 @@ class Mesa
845
697
  end
846
698
  end
847
699
 
848
- def log_summary(mod: :all)
700
+ def all_names_ordered(mod: :all)
701
+ load_test_source_data unless @names_to_numbers
849
702
  if mod == :all
850
- MesaTestCase.modules.each do |this_mod|
851
- log_summary(mod: this_mod)
703
+ # build up list by first constructing each modules list and then
704
+ # concatenating them
705
+ MesaTestCase.modules.inject([]) do |res, this_mod|
706
+ res += all_names_ordered(mod: this_mod)
852
707
  end
853
708
  else
854
709
  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))
710
+ res = Array.new(@names_to_numbers[mod].length, '')
711
+
712
+ # values of the hash give their order, keys are the names, so
713
+ # we assign keys to positions in the array according to their value
714
+ @names_to_numbers[mod].each_pair do |key, val|
715
+ res[val - 1] = key # +list_tests+ gives 1-indexed positions
871
716
  end
717
+ res
872
718
  end
873
719
  end
874
720
 
875
721
  def find_test_case_by_name(test_case_name: nil, mod: :all)
722
+ load_test_source_data unless @names_to_numbers
876
723
  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]
724
+ # look through all loaded modules for desired test case name, only
725
+ # return a test case if a single case is found with that name
726
+ case all_names_ordered.count(test_case_name)
727
+ when 1
728
+ # it exists in exactly one module, but we need to find the module
729
+ # and then return the +MesaTestCase+ object
730
+ MesaTestCase.modules.each do |this_mod|
731
+ if @test_case_names[this_mod].include? test_case_name
732
+ # found it, return the appropriate object
733
+ return @test_cases[this_mod][test_case_name]
734
+ end
882
735
  end
736
+ raise 'Weird problem: found test case in overall names, but '\
737
+ "not in any particular module. This shouldn't happen."
738
+ when 0
739
+ raise(TestCaseDirError, "Could not find test case #{test_case_name} "\
740
+ 'in any module.')
741
+ else
742
+ raise(TestCaseDirError, 'Found multiple test cases named '\
743
+ "#{test_case_name} in multiple modules. Indicate the module you "\
744
+ 'want to search.')
883
745
  end
884
- # didn't find any matches, return nil
885
- nil
746
+ # append this array to the end of the exisitng one
886
747
  else
887
748
  # module specified; check it and return the proper test case (may be nil
888
749
  # if the test case doesn't exist)
889
750
  check_mod mod
890
- @test_cases[mod][test_case_name]
751
+ if @test_case_names[mod].include? test_case_name
752
+ # happy path: test case exists in the specified module
753
+ return @test_cases[mod][test_case_name]
754
+ else
755
+ raise TestCaseDirError.new('Could not find test case ' \
756
+ "#{test_case_name} in the #{mod} module.")
757
+ end
891
758
  end
892
759
  end
893
760
 
@@ -895,149 +762,69 @@ class Mesa
895
762
  # this will be the index in the name array of the proper module of
896
763
  # the desired test case
897
764
  # input numbers are 1-indexed, but we'll fix that later
898
- return nil if test_number < 1
899
- i = test_number
765
+ if test_number < 1 || test_number > test_case_count(mod: mod)
766
+ raise TestCaseDirError.new('Invalid test case number for searching '\
767
+ "in module #{mod}. Must be between 1 and #{test_case_count(mod: mod)}.")
768
+ end
900
769
 
901
770
  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
771
+ # can get the name easily, now need to find the module
772
+ test_case_name = all_names_ordered[test_number - 1]
773
+ MesaTestCase.modules.each do |mod|
774
+ if test_number <= test_case_count(mod: mod)
775
+ # test must live in this module; we have everything
776
+ return MesaTestCase.new(
777
+ test: test_case_name,
778
+ mod: mod,
779
+ mesa: self,
780
+ position: @names_to_numbers[mod][test_case_name]
914
781
  )
782
+ else
783
+ # number was too big, so decrement by this modules case count
784
+ # and move on to next one
785
+ test_number -= test_case_count(mod: mod)
915
786
  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
787
  end
921
- # return nil if we never broke out of the loop
922
- nil
788
+ # should return before we get here, but fail hard if we do
789
+ raise TestCaseDirError.new('Unknown problem in loading test case #' +
790
+ test_number + '.')
923
791
  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
792
+ # module was specified, so we can get at everything right away
793
+ check_mod mod
794
+ return MesaTestCase.new(
795
+ test: all_names_ordered(mod: mod)[test_number - 1],
796
+ mod: mod,
797
+ mesa: self,
798
+ position: test_number
931
799
  )
932
800
  end
933
801
  end
934
802
  end
935
803
 
936
804
  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
805
+ attr_reader :test_name, :mesa, :mod, :position, :shell
945
806
 
946
807
  def self.modules
947
808
  %i[star binary astero]
948
809
  end
949
810
 
950
- def initialize(test: nil, mesa: nil, success_string: '',
951
- final_model: 'final.mod', photo: nil, mod: nil)
811
+ def initialize(test: nil, mesa: nil, mod: nil, position: nil)
952
812
  @test_name = test
953
- @mesa_dir = mesa.mesa_dir
954
813
  @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
814
  unless MesaTestCase.modules.include? mod
992
815
  raise TestCaseDirError, "Invalid module: #{mod}. Must be one of: " +
993
816
  MesaTestCase.modules.join(', ')
994
817
  end
995
818
  @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"
819
+ @position = position
1010
820
 
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
- }
821
+ # way to output colored text to shell
822
+ @shell = Thor::Shell::Color.new
1020
823
 
1021
824
  # validate stuff
1022
825
  check_mesa_dir
1023
826
  check_test_case
1024
827
 
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
828
  end
1042
829
 
1043
830
  def test_suite_dir
@@ -1048,214 +835,26 @@ class MesaTestCase
1048
835
  File.join(test_suite_dir, test_name)
1049
836
  end
1050
837
 
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
838
+ # just punt to +each_test_run+ in the test_suite directory. It's your problem
839
+ # now, sucker!
1107
840
  def do_one
1108
841
  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
842
+ visit_dir(test_suite_dir) do
843
+ bash_execute("./each_test_run #{position}")
1173
844
  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
845
  end
1177
846
 
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
847
+ def results_hash
848
+ testhub_file = File.join(test_case_dir, 'testhub.yml')
849
+ unless File.exist?(testhub_file)
850
+ raise TestCaseDirError.new('No results found for test case '\
851
+ "#{test_name}.")
1186
852
  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]
1230
- end
1231
- end
1232
-
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
1244
- end
1245
- {runtime_minutes: runtime_minutes, retries: retries, backups: backups,
1246
- steps: steps}
853
+ YAML.safe_load(File.read(testhub_file), [Symbol])
1247
854
  end
1248
855
 
1249
856
  private
1250
857
 
1251
- def data_types
1252
- %i[float integer string boolean]
1253
- end
1254
-
1255
- def rn_time
1256
- runtime_seconds
1257
- end
1258
-
1259
858
  # cd into the test case directory, do something in a block, then cd back
1260
859
  # to original directory
1261
860
  def in_dir(&block)
@@ -1269,328 +868,10 @@ class MesaTestCase
1269
868
  raise TestCaseDirError, "No such test case: #{test_case_dir}."
1270
869
  end
1271
870
 
1272
- # verify that mesa_dir is valid by checking for version number and test_suite
1273
- # directory
871
+ # "verify" that mesa_dir is valid by checking for test_suite directory
1274
872
  def check_mesa_dir
1275
- is_valid = File.exist?(File.join(mesa_dir, 'data', 'version_number')) &&
1276
- dir_or_symlink_exists?(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 antepenultimate 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
- # sort by filesystem modification time
1411
- sorted_photo_files = photo_files.sort_by { |file_name| File.stat(file_name).mtime }
1412
- # if you can, pull out 3rd most recent one; otherwise take the oldest
1413
- if sorted_photo_files.length >= 3 then
1414
- re_photo = File.basename(sorted_photo_files[-3])
1415
- else
1416
- re_photo = File.basename(sorted_photo_files[0])
1417
- end
1418
- # if binary, trim off prefix
1419
- re_photo = re_photo.sub("b_", "")
1420
- else
1421
- re_photo = photo
1422
- end
1423
-
1424
- # check that photo file actually exists
1425
- unless File.exist?(File.join('photos', re_photo)) ||
1426
- File.exist?(File.join('photos1', re_photo)) ||
1427
- File.exist?(File.join('photos', "b_#{re_photo}"))
1428
- return fail_test(:photo_file)
1429
- end
1430
-
1431
- # remove final model since it will be remade by restart
1432
- FileUtils.rm_f final_model
1433
-
1434
- # do restart and consolidate output. Command depends on if we have access
1435
- # to SDK version of gnu time.
1436
- re_command = if ENV['MESASDK_ROOT'] && File.exist?(File.join(ENV['MESASDK_ROOT'], 'bin', 'time'))
1437
- %q(command time -f '%M' -o mem-re.txt ./re ) + "#{re_photo}" \
1438
- ' >> out.txt 2> err.txt'
1439
- else
1440
- "./re #{re_photo} >> out.txt 2> err.txt"
1441
- end
1442
-
1443
- puts re_command
1444
- # puts "./re #{re_photo} >> out.txt 2> err.txt"
1445
- re_start = Time.now
1446
- # bash_execute("./re #{re_photo} >> out.txt 2> err.txt")
1447
- # bash_execute(%Q{command time -f '%M' -o mem-re.txt ./re #{re_photo} >> out.txt 2> err.txt})
1448
- bash_execute(re_command)
1449
- re_finish = Time.now
1450
- @re_time = (re_finish - re_start).to_i
1451
- append_and_rm_err
1452
-
1453
- # check that final model matches
1454
- puts './ck >& final_check_diff.txt'
1455
- return fail_test(:photo_checksum) unless
1456
- bash_execute('./ck >& final_check_diff.txt')
1457
- return fail_test(:photo_diff) if
1458
- File.exist?('final_check_diff.txt') &&
1459
- !File.read('final_check_diff.txt').empty?
1460
- succeed(:photo_checksum)
1461
- end
1462
-
1463
- def build_and_run
1464
- # assumes we are in the test case directory. Should only be called
1465
- # in the context of an `in_dir` block.
1466
-
1467
- # first clean and make... Should be compatible with any shell since
1468
- # redirection is always wrapped in 'bash -c "{STUFF}"'
1469
- simple_clean
1470
- begin
1471
- mk
1472
- rescue TestCaseDirError
1473
- return fail_test(:compilation)
1474
- end
1475
-
1476
- # remove old final model if it exists
1477
- remove_final_model
1478
-
1479
- # only check restart/photo if we get through run successfully
1480
- check_restart if check_run
1481
-
1482
- # get reported runtime, retries, backups, and steps
1483
- load_summary_data if File.exist?(out_file)
1484
- end
1485
-
1486
- # append contents of err.txt to end of out.txt, then delete err.txt
1487
- def append_and_rm_err(outfile = 'out.txt', errfile = 'err.txt')
1488
- err_contents = File.read(errfile)
1489
- display_errors(err_contents)
1490
- log_errors(err_contents, outfile)
1491
- FileUtils.rm errfile
1492
- end
1493
-
1494
- def display_errors(err_contents)
1495
- return if err_contents.strip.empty?
1496
- shell.say("\nERRORS", :red)
1497
- puts err_contents
1498
- shell.say('END OF ERRORS', :red)
1499
- end
1500
-
1501
- def log_errors(err_contents, outfile)
1502
- return if err_contents.strip.empty?
1503
- File.open(outfile, 'a') { |f_out| f_out.write(err_contents) }
1504
- shell.say("appended to #{outfile}\n", :red)
1505
- end
1506
-
1507
- def simple_clean
1508
- puts './clean'
1509
- return if bash_execute('./clean')
1510
- raise TestCaseDirError, 'Encountered an error when running `clean` in ' \
1511
- "#{Dir.getwd} for test case #{test_name}."
1512
- end
1513
-
1514
- def mk
1515
- puts './mk > mk.txt'
1516
- unless bash_execute('./mk > mk.txt')
1517
- raise TestCaseDirError, 'Encountered an error when running `mk` in ' \
1518
- "#{Dir.getwd} for test case #{test_name}."
1519
- end
1520
- FileUtils.rm 'mk.txt'
1521
- end
1522
-
1523
- def remove_final_model
1524
- # remove final model if it already exists
1525
- return unless final_model
1526
- return unless File.exist?(final_model)
1527
- FileUtils.rm(final_model)
1528
- end
1529
-
1530
- def out_file
1531
- File.join(test_case_dir, 'out.txt')
1532
- end
1533
-
1534
- # helpers for getting run summaries
1535
- def run_summaries
1536
- # look at all lines in out.txt
1537
- lines = IO.readlines(out_file)
1538
-
1539
- # find lines with summary information
1540
- summary_line_numbers = []
1541
- lines.each_with_index do |line, i|
1542
- if line =~ /^\s*runtime \(minutes\),\s+retries,\s+backups,\ssteps/
1543
- summary_line_numbers << i
1544
- end
1545
- end
1546
-
1547
- # find lines indicating passage or failure of runs and restarts
1548
- run_finish_line_numbers = []
1549
- restart_finish_line_numbers = []
1550
- lines.each_with_index do |line, i|
1551
- if line =~ /^\s*((?:PASS)|(?:FAIL))\s+#{test_name}\s+restart/
1552
- restart_finish_line_numbers << i
1553
- elsif line =~ /^\s*((?:PASS)|(?:FAIL))\s+#{test_name}\s+run/
1554
- run_finish_line_numbers << i
1555
- end
1556
- end
1557
-
1558
- # only keep summaries that correspond to runs rather than restart
1559
- summary_line_numbers.select do |i|
1560
- run_summary?(i, run_finish_line_numbers, restart_finish_line_numbers)
1561
- end.map { |line_number| lines[line_number] }
1562
- end
1563
-
1564
- def get_summary_text
1565
- # original plan was to include diff data in summary text... now it's just
1566
- # part of the test_instance object and is submitted as an integer
1567
- # res = case diff
1568
- # when 0
1569
- # "No Diff\n"
1570
- # when 1
1571
- # "Diff\n"
1572
- # else
1573
- # "Ambiguous Diff\n"
1574
- # end
1575
- # res +
1576
- IO.readlines(out_file).select do |line|
1577
- line =~ /^\s*runtime/
1578
- end.join
1579
- end
1580
-
1581
- def run_summary?(i, run_finish_line_numbers, restart_finish_line_numbers)
1582
- # iterate from starting line (a summary line) up to largest PASS/FAIL
1583
- # line, bail out if summary line is beyond any PASS/FAIL line
1584
- max_line = run_finish_line_numbers.max || 0
1585
- max_line = [max_line, (restart_finish_line_numbers.max || 0)].max
1586
- return false if i > max_line
1587
- # return true if next PASS/FAIL line is for a run and fail if it is for a
1588
- # restart
1589
- i.upto(max_line) do |j|
1590
- return true if run_finish_line_numbers.include?(j)
1591
- return false if restart_finish_line_numbers.include?(j)
1592
- end
1593
- false
873
+ is_valid = dir_or_symlink_exists? test_suite_dir
874
+ raise MesaDirError, "Invalid MESA dir: #{mesa.mesa_dir}" unless is_valid
1594
875
  end
1595
876
 
1596
877
  end
@@ -1605,13 +886,11 @@ end
1605
886
  def visit_and_check(new_dir, exception, message)
1606
887
  cwd = Dir.getwd
1607
888
  shell.say "Leaving #{cwd}", :blue
1608
- puts ''
1609
- shell.say "Entering #{new_dir}.", :blue
889
+ shell.say "\nEntering #{new_dir}.", :blue
1610
890
  Dir.chdir(new_dir)
1611
891
  success = yield if block_given?
1612
892
  shell.say "Leaving #{new_dir}", :blue
1613
- puts ''
1614
- shell.say "Entering #{cwd}.", :blue
893
+ shell.say "\nEntering #{cwd}.", :blue
1615
894
  Dir.chdir(cwd)
1616
895
  return if success
1617
896
  raise exception, message
@@ -1619,18 +898,18 @@ end
1619
898
 
1620
899
  # cd into a new directory, execute a block, then cd back into original
1621
900
  # directory
1622
- def visit_dir(new_dir)
901
+ def visit_dir(new_dir, quiet: false)
1623
902
  cwd = Dir.getwd
1624
- shell.say "Leaving #{cwd}\n", :blue
1625
- shell.say "Entering #{new_dir}.", :blue
903
+ shell.say "Leaving #{cwd}\n", :blue unless quiet
904
+ shell.say "\nEntering #{new_dir}.", :blue unless quiet
1626
905
  Dir.chdir(new_dir)
1627
906
  yield if block_given?
1628
- shell.say "Leaving #{new_dir}\n", :blue
1629
- shell.say "Re-entering #{cwd}.", :blue
1630
- puts ""
907
+ shell.say "Leaving #{new_dir}\n", :blue unless quiet
908
+ shell.say "\nRe-entering #{cwd}.", :blue unless quiet
1631
909
  Dir.chdir(cwd)
1632
910
  end
1633
911
 
912
+ # the next function probalby doesn't belong here, but keep it anyway, please
1634
913
  # create seed data for test cases for MesaTestHub of a given mesa version
1635
914
  def generate_seeds_rb(mesa_dir, outfile)
1636
915
  m = Mesa.new(mesa_dir: mesa_dir)
@@ -1641,7 +920,6 @@ def generate_seeds_rb(mesa_dir, outfile)
1641
920
  m.test_names.each do |test_case_name|
1642
921
  f.puts ' {'
1643
922
  f.puts " name: '#{test_case_name}',"
1644
- f.puts " version_added: #{m.version_number},"
1645
923
  # no comma on last one
1646
924
  if test_case_name == m.test_names[-1]
1647
925
  f.puts(' }')
@@ -1654,12 +932,22 @@ def generate_seeds_rb(mesa_dir, outfile)
1654
932
  end
1655
933
  end
1656
934
 
1657
- # Check if path is directory or symlink
1658
935
  def dir_or_symlink_exists?(path)
1659
- File.directory?(path) || File.symlink?(path)
936
+ File.directory?(path) || File.symlink?(path)
1660
937
  end
1661
938
 
1662
939
  # force the execution to happen with bash
1663
- def bash_execute(command)
1664
- system('bash -c "' + command + '"')
940
+ def bash_execute(command, throw_exception=false)
941
+ res = system('bash -c "' + command + '"')
942
+ if !res && throw_exception
943
+ raise BashError('Encountered an error when executing the following '\
944
+ "command in bash: #{command}.")
945
+ end
946
+ res
947
+ end
948
+
949
+ # force execution to happen with bash, but return result rather than exit
950
+ # status (like backticks)
951
+ def bashticks(command)
952
+ `bash -c "#{command}"`.chomp
1665
953
  end