mesa_test 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mesa_test +673 -136
  3. data/lib/mesa_test.rb +734 -295
  4. metadata +9 -5
data/bin/mesa_test CHANGED
@@ -15,37 +15,112 @@ end
15
15
 
16
16
  require 'thor'
17
17
  require 'yaml'
18
+ require 'etc'
18
19
 
19
20
  class MesaTest < Thor
20
21
  DEVELOPMENT_URI = 'http://localhost:3000'.freeze
21
22
  STAGING_URI = 'https://beta-testhub.herokuapp.com'.freeze
22
23
 
24
+ # Exit status when MESATestHub has nothing for this computer to do, so a
25
+ # script can stop instead of re-testing whatever is already in the work
26
+ # directory: mesa_test install best || exit
27
+ NOTHING_TO_DO_EXIT = 3
28
+
29
+ def self.exit_on_failure?
30
+ true
31
+ end
32
+
33
+ # --claim / --no-claim, shared by every command that starts work.
34
+ def self.claim_option
35
+ option :claim, desc: 'Tell MESATestHub this computer is working on the ' \
36
+ 'commit (and its tests) so it shows as pending, not ' \
37
+ 'untested. Use --no-claim if something else already ' \
38
+ 'claimed it.',
39
+ type: :boolean, default: true
40
+ end
41
+
42
+ # Parallel testing (opt-in; with neither set, tests run serially in your
43
+ # shell's environment exactly as before).
44
+ def self.parallel_options
45
+ option :jobs, desc: 'Run this many tests at once (each in its own ' \
46
+ 'process; output in each_test_run.log in the test ' \
47
+ 'directory). Default 1: one after another.',
48
+ type: :numeric, default: 1, aliases: '-j'
49
+ option :test_threads, desc: 'OMP_NUM_THREADS for each test. Default: ' \
50
+ "your shell's OMP_NUM_THREADS (else all cores) " \
51
+ 'split evenly across --jobs.',
52
+ type: :numeric
53
+ end
54
+
55
+ # NPROCS (MESA's install runs make -j$NPROCS; all cores if unset) and
56
+ # OMP_NUM_THREADS for the build.
57
+ def self.build_threads_option
58
+ option :build_threads, desc: 'Cores for compiling MESA (sets NPROCS and ' \
59
+ 'OMP_NUM_THREADS for the install; default: ' \
60
+ 'all cores).',
61
+ type: :numeric
62
+ end
63
+
64
+ # The run-mode switches. Each is three-way: --x, --no-x, or unset. What
65
+ # unset means depends on the command (see each command's help).
66
+ def self.run_mode_options
67
+ option :skip_optional, desc: 'Skip optional inlists (sets MESA_SKIP_OPTIONAL); ' \
68
+ '--no-skip-optional runs them all, as MESA does ' \
69
+ 'by default.',
70
+ type: :boolean
71
+ option :fpe, desc: 'Floating-point exception checks (MESA_FPE_CHECKS_ON=1). ' \
72
+ 'Compiled in, so it is chosen when MESA is installed.',
73
+ type: :boolean
74
+ option :converge, desc: 'Convergence run (sets MESA_TEST_SUITE_RESOLUTION_FACTOR, ' \
75
+ "keeping your value if set, else #{DEFAULT_CONVERGE_FACTOR}).",
76
+ type: :boolean
77
+ option :resolution_factor, desc: 'Convergence run at this factor (implies --converge). ' \
78
+ 'With `best`, a factor the commit names ' \
79
+ '([ci converge 0.5]) wins.',
80
+ type: :numeric
81
+ end
82
+
23
83
  desc 'test [TEST CASE]', 'run and submit a test case or commit'
24
84
  long_desc <<-LONGDESC
25
- If you specify an optional test case, only that test case
26
- will be tested. Otherwise all test cases in your work directory (set up in
27
- your .mesa_test.yml file). TEST_CASE can also be an integer corresponding
28
- to the position it has in do1_test_source (or a concatenated version of
29
- that file if no module is specified). Modules are searched/concatenated in
30
- this order:
85
+ Runs TEST_CASE, or with no TEST_CASE every test case, in the MESA
86
+ installation in your work directory (mesa_work in ~/.mesa_test/config.yml,
87
+ or --mesadir). TEST_CASE can be a name or an integer: its position in
88
+ do1_test_source (or in the modules' lists concatenated, if no --module is
89
+ given). Modules are searched/concatenated in this order:
31
90
 
32
91
  #{MesaTestCase.modules.map { |mod| ' ' + mod.to_s }.join(', ')}
92
+
93
+ Each test is claimed on MESATestHub as it starts, so it shows as pending,
94
+ and its result is submitted as soon as it finishes. This needs an API key
95
+ (see `mesa_test help setup`). --no-submit runs tests locally only:
96
+ nothing is claimed or sent, and no key is needed.
97
+
98
+ With no TEST_CASE, --jobs N runs N tests at once.
99
+
100
+ Run modes (--skip-optional, --fpe, --converge): a flag given here wins;
101
+ otherwise the mode recorded when this build was installed (in
102
+ $MESA_DIR/testhub.yml) applies; otherwise your shell's environment
103
+ does. FPE checks are compiled in, so a build installed with or without
104
+ them keeps that setting whatever is passed here.
33
105
  LONGDESC
34
106
 
35
- option :force, desc: 'When submitting, assume submission data in ' \
36
- '~/.mesa_test.yml are correct rather than checking ' \
37
- 'with user.',
107
+ option :force, desc: 'Use ~/.mesa_test/config.yml as it is. --no-force ' \
108
+ 'shows it and asks you to confirm (or fix) it first.',
38
109
  type: :boolean, default: true, aliases: '-f'
39
110
  option 'force-logs', desc: 'Submit logs for test case(s), even if they pass',
40
111
  type: :boolean, default: false
41
- option :submit, desc: 'Submit results (if any) to MesaTestHub.',
112
+ option :submit, desc: 'Submit results (if any) to MESATestHub.',
42
113
  type: :boolean, default: true
43
114
  option :module, desc: 'Which module to test and/or submit, if applicable.',
44
115
  type: :string, default: 'all', aliases: '-m'
45
116
  option :mesadir, desc: 'Location of mesa dir, if not relying on config data',
46
117
  type: :string
118
+ claim_option
119
+ run_mode_options
120
+ parallel_options
47
121
 
48
122
  def test(test_case_name = nil)
123
+ validate_options!
49
124
  s = if options[:submit]
50
125
  create_and_check_submitter(force: options[:force])
51
126
  else
@@ -57,58 +132,57 @@ class MesaTest < Thor
57
132
  mod: options[:module].downcase.to_sym,
58
133
  github_protocol: s.github_protocol
59
134
  )
60
- m.with_mesa_dir do
61
- if test_case_name
62
- # only testing one test case
63
- t = create_and_check_test_case(mesa: m, test_case_name: test_case_name,
64
- mod: options[:module].downcase.to_sym)
65
-
66
- # run test
67
- t.do_one
68
-
69
- # bail out if not submitting
70
- return unless options[:submit]
71
-
72
- # submit results
73
- shell.say 'Submitting results to ' + s.base_uri + '... ', :blue
74
- s.submit_instance(m, t, force_logs: options['force-logs'])
75
- else
76
- # run all tests
77
- m.each_test_run(mod: options[:module].downcase.to_sym)
78
-
79
- # bail out if not submitting
80
- return unless options[:submit]
81
-
82
- shell.say 'Submitting results to ' + s.base_uri + '... ', :blue
83
- s.submit_commit(m, force_logs: options['force-logs'])
135
+ modes = test_run_modes(m)
136
+ with_env(run_mode_env(modes).merge(test_thread_env)) do
137
+ m.with_mesa_dir do
138
+ if test_case_name
139
+ # only testing one test case
140
+ t = create_and_check_test_case(mesa: m, test_case_name: test_case_name,
141
+ mod: options[:module].downcase.to_sym)
142
+
143
+ if options[:submit] && options[:claim]
144
+ s.claim(sha: m.sha, scope: 'test', mod: t.mod, test_case: t.test_name,
145
+ flags: claim_flags(modes), mesa: m)
146
+ end
147
+
148
+ say_step(shell, "Testing #{t.mod}/#{t.test_name}")
149
+ started = Time.now
150
+ t.do_one
151
+ finish_test(s, m, t, started, submit: options[:submit])
152
+ else
153
+ # Each test is claimed as it starts and submitted as it finishes.
154
+ # (`install` reports the build.)
155
+ run_tests(s, m, mod: options[:module].downcase.to_sym,
156
+ submit: options[:submit], flags: claim_flags(modes))
157
+ end
84
158
  end
85
159
  end
86
- shell.say "Done.\n", :green
87
160
  end
88
161
 
89
162
  desc 'submit [TEST CASE]', 'submit a test case or commit'
90
163
  long_desc <<-LONGDESC
91
- All tests are assumed to be in the default mesa directory (set as mesa_work
92
- in .mesa_test.yml. If you specify an optional test case, only that test case
93
- will be submitted. TEST_CASE can also be an integer corresponding to the
94
- position it has in do1_test_source (or a concatenated version of that file
95
- if no module is specified). Modules are searched/concatenated in this
96
- order:
164
+ Submits results already in your work directory (mesa_work in
165
+ ~/.mesa_test/config.yml, or --mesadir) without running anything: say, from
166
+ a cluster's head node after jobs ran with --no-submit, or to retry after a
167
+ network failure. With TEST_CASE (a name or number, as for `test`), just
168
+ that test's result. Without it, the build status plus every test with
169
+ results from this build (or with --empty, just the build status), in one
170
+ request. Results left over from an earlier build are skipped, and
171
+ MESATestHub ignores results it already has, so submitting again is safe.
172
+ Needs an API key (see `mesa_test help setup`). Modules are
173
+ searched/concatenated in this order:
97
174
 
98
175
  #{MesaTestCase.modules.map { |mod| ' ' + mod.to_s }.join(', ')}
99
176
  LONGDESC
100
177
 
101
- option :force, desc: 'When submitting, assume submission data in ' \
102
- '~/.mesa_test.yml are correct rather than checking ' \
103
- 'with user.',
178
+ option :force, desc: 'Use ~/.mesa_test/config.yml as it is. --no-force ' \
179
+ 'shows it and asks you to confirm (or fix) it first.',
104
180
  type: :boolean, default: true, aliases: '-f'
105
181
  option 'force-logs', desc: 'Submit logs for test case(s), even if they pass',
106
182
  type: :boolean, default: false
107
183
  option :module, desc: 'Which module to test and/or submit, if applicable.',
108
184
  type: :string, default: 'all', aliases: '-m'
109
- option :empty, desc: 'Whether any test cases will be submitted. Useful for '\
110
- 'indicating compilation status independently of ' \
111
- 'single test submissions.',
185
+ option :empty, desc: 'Submit only the build status, no test results.',
112
186
  type: :boolean, default: false, aliases: '-e'
113
187
  option :mesadir, desc: 'Location of mesa dir, if not relying on config data',
114
188
  type: :string
@@ -133,36 +207,32 @@ class MesaTest < Thor
133
207
 
134
208
  t = create_and_check_test_case(mesa: m, test_case_name: test_case_name,
135
209
  mod: options[:module].downcase.to_sym)
136
- # submit results
137
- shell.say 'Submitting results to ' + s.base_uri + '... ', :blue
210
+ raise Thor::Error, "#{t.mod}/#{t.test_name} has no results to submit." unless t.ran?
211
+
138
212
  s.submit_instance(m, t, force_logs: options['force-logs'])
139
- shell.say "Done.\n", :green
140
213
  else
141
- # submitting compilation information and either all tests (entire) or
142
- # no tests (empty)
214
+ # The build status plus (unless --empty) every test with results from
215
+ # this build, in one request. MESATestHub stores it as a build row
216
+ # plus one row per test and ignores anything it already has (by
217
+ # fingerprint), so re-running `submit` is safe.
143
218
  begin
144
219
  m.check_installation
145
220
  rescue MesaDirError
146
- shell.say %q{This MESA installation doesn't seem to be compiled } \
147
- 'properly. Attempting to submit a compilation failure to '\
148
- 'MESATestHub.', :red
221
+ shell.say "This build didn't finish compiling, so only the failed build " \
222
+ 'will be reported.', :yellow
149
223
  empty = true
150
- ensure
151
- # submit all tests
152
- s.submit_commit(m, empty: empty, force_logs: options['force-logs'])
153
224
  end
225
+ s.submit_commit(m, empty: empty, force_logs: options['force-logs'])
154
226
  end
155
227
  end
156
228
  end
157
229
 
158
230
  desc 'checkout [SHA]', 'checkout a commit'
159
231
  long_desc <<-LONGDESC
160
- Checks out a particular commit to default mesa directory (mesa_work in
161
- .mesa_test.yml). Uses git worktree magic to avoid re-downloading all of
162
- mesa history if a mirror repo is already set up. Does not affect the
163
- user's MESA_DIR or other environment variables.
164
-
165
- If no sha is provided, defaults to main (if checkout is occuring).
232
+ Checks out SHA (default: main) into your work directory (mesa_work in
233
+ ~/.mesa_test/config.yml, or --mesadir), using git worktrees off the mirror
234
+ so MESA's history isn't downloaded again. Doesn't build, doesn't contact
235
+ MESATestHub, and doesn't touch your own MESA_DIR or environment.
166
236
  LONGDESC
167
237
 
168
238
  option :mesadir, desc: 'Location of mesa dir, if not relying on config data',
@@ -180,16 +250,30 @@ class MesaTest < Thor
180
250
 
181
251
  desc 'install [SHA]', 'checkout and install a commit'
182
252
  long_desc <<-LONGDESC
183
- Optionally checks out a particular commit to default mesa directory
184
- (mesa_work in .mesa_test.yml). Then runs installation script in the work
185
- directory. Equivalent to an [optional] git checkout followed by going
186
- into the directory and running ./clean and ./install, but there is some git
187
- worktree magic on the backend to improve efficiency. SDK or compilers must be
188
- set up prior. Does not affect the user's MESA_DIR or other environment
189
- variables outside of execution.
190
-
191
- If no sha is provided, defaults to main (if checkout is occuring). SHA is
192
- ignored if using option --no-checkout
253
+ Checks out SHA (default: main) into your work directory (mesa_work in
254
+ ~/.mesa_test/config.yml, or --mesadir) and builds it: ./clean and
255
+ ./install, with git worktrees off the mirror so MESA's history isn't
256
+ downloaded again. The SDK or compilers must already be set up. Doesn't
257
+ touch your own MESA_DIR or environment. With --no-checkout, builds what's
258
+ already there and ignores SHA.
259
+
260
+ The build is claimed and its result (pass or fail, with the build log)
261
+ reported to MESATestHub, which needs an API key (see `mesa_test help
262
+ setup`). A failed build exits with status 1. --no-submit just builds, for
263
+ local work: nothing is claimed or sent, and no key is needed.
264
+
265
+ Run modes given here (--skip-optional, --fpe, --converge) are used for
266
+ the build and recorded in $MESA_DIR/testhub.yml, so later `mesa_test test`
267
+ runs on this build (e.g. cluster array jobs) apply them automatically.
268
+
269
+ If SHA is `best`, MESATestHub picks the commit this computer should build
270
+ next, and the run modes come from its request instead: the mode flags then
271
+ say what this computer is *willing* to do (overriding `capabilities` in the
272
+ config file). If nothing needs building, mesa_test exits with status
273
+ #{NOTHING_TO_DO_EXIT} without touching the work directory, so a script can
274
+ stop:
275
+
276
+ mesa_test install best || exit
193
277
  LONGDESC
194
278
 
195
279
  option :checkout, desc: 'Whether a checkout needs to happen. Use '\
@@ -197,12 +281,30 @@ class MesaTest < Thor
197
281
  type: :boolean, default: true, aliases: '-c'
198
282
  option :mesadir, desc: 'Location of mesa dir, if not relying on config data',
199
283
  type: :string
284
+ option :submit, desc: 'Report the build (claim it first, then submit its ' \
285
+ 'status) to MESATestHub. --no-submit just builds, ' \
286
+ 'e.g. for local testing.',
287
+ type: :boolean, default: true
288
+ claim_option
289
+ run_mode_options
290
+ build_threads_option
200
291
 
201
292
  def install(sha = 'main')
293
+ validate_options!
202
294
  # assuming config wizard has already been set (perhaps a little reckless)
203
295
  # so that we can get at the work directory and mirror directory
204
- s = create_submitter(force: true)
205
- m = if options[:checkout]
296
+ s = if options[:submit] || sha == 'best'
297
+ create_and_check_submitter(force: true)
298
+ else
299
+ create_submitter(force: true)
300
+ end
301
+ rec = nil
302
+ if sha == 'best'
303
+ rec = dispatch_build_or_exit(s)
304
+ sha = rec['commit_sha']
305
+ end
306
+ modes = rec ? modes_for(rec) : explicit_run_modes
307
+ m = if options[:checkout] || rec
206
308
  Mesa.checkout(
207
309
  sha: sha,
208
310
  work_dir: work_dir(options[:mesadir], s.mesa_work),
@@ -217,36 +319,51 @@ class MesaTest < Thor
217
319
  github_protocol: s.github_protocol
218
320
  )
219
321
  end
220
- begin
221
- m.clean
222
- m.install
322
+ if options[:submit] && options[:claim]
323
+ s.claim(sha: m.sha, scope: 'build', flags: claim_flags(modes),
324
+ dispatched_at: rec && rec['dispatched_at'])
325
+ end
326
+ built = begin
327
+ build_with_modes(m, modes, raise_on_failure: true)
328
+ true
223
329
  rescue MesaDirError
224
- shell.say "\nFailed in compiling MESA.", :red
225
- else
226
- shell.say "\nSuccessfully compiled MESA commit #{m.sha}.", :green
330
+ false
227
331
  end
332
+ # Report the build either way (a failure carries its build log), like
333
+ # `test` reports each test.
334
+ submit_build(s, m) if options[:submit]
335
+ exit 1 unless built
228
336
  end
229
337
 
230
338
  desc 'install_and_test [SHA]', 'Install, test, and submit an entire commit.'
231
339
  long_desc <<-LONGDESC
232
- Optionally checks out a particular commit (specified by SHA, which default to
233
- HEAD if not provided) to a mesa directory (mesa_work in .mesa_test.yml,
234
- unless a custom location is provided by --workdir=MYDIR). Then go into the
235
- directory and run ./clean and ./install. SDK or compilers must be set up
236
- prior. Once installation is complete, run the test suite, and report results
237
- (commit and all test data) to MesaTestHub in a single request.
238
- Does not affect the user's MESA_DIR or other environment variables. This is
239
- basically a shortcut for running
240
-
241
- `mesa_test install SOME_SHA`
242
-
243
- followed by
244
-
245
- `mesa_test test`
246
-
247
- If no sha is provided, defaults to main (if checkout is occuring). SHA is
248
- ignored if using option --no-checkout
249
-
340
+ Checks out SHA (default: main) into your work directory (mesa_work in
341
+ ~/.mesa_test/config.yml, or --mesadir), builds it (./clean and ./install;
342
+ the SDK or compilers must already be set up), and runs the test suite.
343
+ Doesn't touch your own MESA_DIR or environment. Roughly a shortcut for
344
+
345
+ mesa_test install SHA && mesa_test test
346
+
347
+ The build status is submitted as soon as the build finishes (a failed
348
+ build is reported and exits 1), then each test is claimed as it starts
349
+ and submitted as it finishes, so results roll in during the run. This
350
+ needs an API key (see `mesa_test help setup`). --no-submit builds and
351
+ tests locally without claiming or sending anything. With --no-checkout,
352
+ the build in the work directory is used and SHA is ignored.
353
+
354
+ --jobs N runs N tests at once; see also --test-threads and --build-threads.
355
+
356
+ Run modes (--skip-optional, --fpe, --converge) apply to the build and
357
+ every test; unset ones are left to your shell's environment.
358
+
359
+ If SHA is `best`, MESATestHub picks the commit (one this computer hasn't
360
+ tested yet) and the run modes (FPE checks, plus full inlists or a
361
+ convergence run if the commit asks for one); everything else is the
362
+ ordinary run above. The mode flags then say what this computer is
363
+ willing to do (overriding `capabilities` in the config file): commits
364
+ asking for [ci optional] or [ci converge] get those runs only if it's
365
+ willing. --no-submit doesn't apply in this mode. If nothing needs
366
+ testing, mesa_test exits with status #{NOTHING_TO_DO_EXIT}.
250
367
  LONGDESC
251
368
 
252
369
  option :checkout, desc: 'Whether a checkout needs to happen. Use '\
@@ -254,18 +371,24 @@ class MesaTest < Thor
254
371
  type: :boolean, default: true, aliases: '-c'
255
372
  option :mesadir, desc: 'Location of mesa dir, if not relying on config data',
256
373
  type: :string
257
- option :force, desc: 'When submitting, assume submission data in ' \
258
- '~/.mesa_test.yml are correct rather than checking ' \
259
- 'with user.',
374
+ option :force, desc: 'Use ~/.mesa_test/config.yml as it is. --no-force ' \
375
+ 'shows it and asks you to confirm (or fix) it first.',
260
376
  type: :boolean, default: true, aliases: '-f'
261
- option :submit, desc: 'Submit results (if any) to MesaTestHub.',
377
+ option :submit, desc: 'Submit results (if any) to MESATestHub.',
262
378
  type: :boolean, default: true
263
379
  option :module, desc: 'Which module to test and/or submit, if applicable.',
264
380
  type: :string, default: 'all', aliases: '-m'
265
381
  option :destroy, type: :boolean, desc: 'Delete mesa installation upon ' \
266
382
  'submission of complete results.', default: false
383
+ claim_option
384
+ run_mode_options
385
+ build_threads_option
386
+ parallel_options
267
387
 
268
388
  def install_and_test(sha='main')
389
+ validate_options!
390
+ return install_and_test_best if sha == 'best'
391
+
269
392
  s = if options[:submit]
270
393
  create_and_check_submitter(force: options[:force])
271
394
  else
@@ -290,38 +413,80 @@ class MesaTest < Thor
290
413
  )
291
414
  end
292
415
 
416
+ modes = explicit_run_modes
417
+ claiming = options[:submit] && options[:claim]
418
+ s.claim(sha: m.sha, scope: 'build', flags: claim_flags(modes)) if claiming
419
+
293
420
  begin
294
- m.clean
295
- m.install
421
+ build_with_modes(m, modes, raise_on_failure: true)
296
422
  rescue MesaDirError
297
- shell.say "\nFailed in compiling MESA.", :red
298
- else
299
- check_mesa(m)
300
- update_mesa(mesa: m, mod: options[:module].downcase.to_sym)
301
-
302
- # run all tests. Don't be fooled, whether or not a diff happens is set
303
- # by the diff attribute of the mesa object, set above.
304
- m.with_mesa_dir { m.each_test_run(mod: options[:module].downcase.to_sym) }
305
- ensure
306
- # submit all commit and test data
307
- successfully_submitted = false
308
- if options[:submit]
309
- successfully_submitted = s.submit_commit(
310
- m, force_logs: options['force-logs'])
423
+ # Report the failed build (with its log) right away.
424
+ submit_build(s, m) if options[:submit]
425
+ exit 1
426
+ end
427
+
428
+ check_mesa(m)
429
+ update_mesa(mesa: m, mod: options[:module].downcase.to_sym)
430
+ # The build's status goes in as soon as it's known, then each test as
431
+ # soon as it finishes -- results roll in rather than arriving in one
432
+ # bulk submission at the end.
433
+ build_submitted = options[:submit] ? submit_build(s, m) : true
434
+ all_submitted = with_env(run_mode_env(m.recorded_run_modes).merge(test_thread_env)) do
435
+ m.with_mesa_dir do
436
+ run_tests(s, m, mod: options[:module].downcase.to_sym,
437
+ submit: options[:submit], flags: claim_flags(modes))
311
438
  end
439
+ end
312
440
 
313
- # if requested, installation and submission are successful, destroy the
314
- # directory
315
- m.remove if successfully_submitted && options[:destroy] && m.installed?
441
+ # if requested, installation and submission are successful, destroy the
442
+ # directory
443
+ if options[:submit] && build_submitted && all_submitted && options[:destroy] && m.installed?
444
+ m.remove
316
445
  end
317
446
  end
318
447
 
319
- desc 'setup [CONFIG_FILE]', 'Setup MesaTestHub config file.'
448
+ desc 'request_work', 'ask MESATestHub what this computer should do next'
320
449
  long_desc <<-LONGDESC
321
- If optional CONFIG_FILE is provided, search for that file and load it
322
- to provide default values for configuration. If not provided, the default
323
- file, ~/.mesa_test.yml, is used. The resulting config file is saved to
324
- the same location it was read from.
450
+ Asks MESATestHub which commit this computer should build next (or, with
451
+ --scope=test --sha=SHA, which test to run next on a commit it built) and
452
+ prints the recommendation as JSON on STDOUT, including the run modes to
453
+ use and the reasons it was picked. The mode flags say what this computer
454
+ is willing to do (overriding `capabilities` in the config file). Prints
455
+ nothing and exits with status #{NOTHING_TO_DO_EXIT} when there is nothing
456
+ useful to do. Read-only: nothing is claimed. Needs an API key (see
457
+ `mesa_test help setup`).
458
+ LONGDESC
459
+
460
+ option :scope, desc: 'build or test', type: :string, default: 'build',
461
+ enum: %w[build test]
462
+ option :sha, desc: 'For --scope=test: the commit you built', type: :string
463
+ run_mode_options
464
+
465
+ def request_work
466
+ s = create_submitter(force: true, hub: true)
467
+ rec = dispatch_or_exit(s, scope: options[:scope], sha: options[:sha])
468
+ exit NOTHING_TO_DO_EXIT unless rec
469
+
470
+ puts JSON.pretty_generate(rec)
471
+ end
472
+
473
+ desc 'setup [CONFIG_FILE]', 'Set up this computer (API key, test modes, directories)'
474
+ long_desc <<-LONGDESC
475
+ Walks through this computer's settings, offering the current ones as
476
+ defaults, and saves them to CONFIG_FILE (default ~/.mesa_test/config.yml,
477
+ which is where the other commands look).
478
+
479
+ mesa_test authenticates with a per-computer API key. To get one, log in
480
+ to MESATestHub, open My Computers, open this computer's page (adding the
481
+ computer first if needed; one used only for `search` and `count` is
482
+ fine), and click "Generate key". The key starts with mth_. Paste it when
483
+ the wizard asks, or export MESATESTHUB_API_KEY, which overrides the
484
+ config file (e.g. to keep the key off disk on a cluster).
485
+
486
+ The wizard checks the key with MESATestHub, which also supplies this
487
+ computer's name. Then it asks which optional test modes the computer is
488
+ willing to run when a commit asks for them (full_inlists, fpe, converge),
489
+ the logs token, and where the MESA mirror and work directory live.
325
490
  LONGDESC
326
491
  def setup(config_file = File.join(ENV['HOME'], '.mesa_test', 'config.yml'))
327
492
  MesaTestSubmitter.new_from_config(config_file: config_file,
@@ -330,9 +495,9 @@ class MesaTest < Thor
330
495
 
331
496
  desc 'search "QUERY"', 'retrieve JSON test instances matching QUERY'
332
497
  long_desc <<-LONGDESC
333
- Sends a GET request to the MesaTestHub search API. QUERY should be
334
- quoted. Credentials are read from your mesa_test config file and sent as
335
- URL query parameters (HTTPS is mandatory).
498
+ Sends a GET request to the MESATestHub search API. QUERY should be
499
+ quoted. Authenticates with your API key; a key for any of your computers
500
+ works, since search is read-only.
336
501
 
337
502
  Query syntax is the same key-value form used by the in-browser search box
338
503
  at testhub.mesastar.org/test_instances/search (see the "Query syntax"
@@ -348,7 +513,7 @@ class MesaTest < Thor
348
513
  LONGDESC
349
514
 
350
515
  def search(query)
351
- s = create_submitter(force: true)
516
+ s = create_submitter(force: true, hub: true)
352
517
  emit_search_response(s.search(query))
353
518
  end
354
519
 
@@ -360,12 +525,365 @@ class MesaTest < Thor
360
525
  LONGDESC
361
526
 
362
527
  def count(query)
363
- s = create_submitter(force: true)
528
+ s = create_submitter(force: true, hub: true)
364
529
  emit_search_response(s.search_count(query))
365
530
  end
366
531
 
367
532
  private
368
533
 
534
+ # `install_and_test best`: MESATestHub picks the commit, then each test.
535
+ def install_and_test_best
536
+ unless options[:submit]
537
+ raise Thor::Error, '`install_and_test best` submits as it goes; ' \
538
+ '--no-submit is not supported.'
539
+ end
540
+ s = create_and_check_submitter(force: options[:force])
541
+ rec = dispatch_build_or_exit(s)
542
+ modes = modes_for(rec)
543
+ m = Mesa.checkout(sha: rec['commit_sha'],
544
+ work_dir: work_dir(options[:mesadir], s.mesa_work),
545
+ mirror_dir: s.mesa_mirror,
546
+ github_protocol: s.github_protocol)
547
+ if options[:claim]
548
+ s.claim(sha: m.sha, scope: 'build', flags: claim_flags(modes),
549
+ dispatched_at: rec['dispatched_at'])
550
+ end
551
+
552
+ begin
553
+ build_with_modes(m, modes, raise_on_failure: true)
554
+ rescue MesaDirError
555
+ submit_build(s, m)
556
+ exit 1
557
+ end
558
+ mod = options[:module].downcase.to_sym
559
+ check_mesa(m)
560
+ update_mesa(mesa: m, mod: mod)
561
+
562
+ # Report the build on its own; this also fulfills the build claim.
563
+ build_submitted = submit_build(s, m)
564
+ # MESATestHub chose the commit and its modes; from here it's an ordinary
565
+ # whole-suite run, as `install_and_test SHA` does it: MESA's order,
566
+ # --jobs at a time, each test claimed as it starts and submitted as it
567
+ # finishes.
568
+ all_submitted = with_env(run_mode_env(m.recorded_run_modes).merge(test_thread_env)) do
569
+ m.with_mesa_dir do
570
+ run_tests(s, m, mod: mod, submit: true, flags: claim_flags(modes))
571
+ end
572
+ end
573
+ m.remove if build_submitted && all_submitted && options[:destroy] && m.installed?
574
+ end
575
+
576
+
577
+ def jobs
578
+ j = options[:jobs].to_i
579
+ raise Thor::Error, '--jobs must be at least 1.' if j < 1
580
+
581
+ j
582
+ end
583
+
584
+ # OMP_NUM_THREADS for each test. --test-threads if given. Otherwise, with
585
+ # --jobs N, the shell's OMP_NUM_THREADS (else the core count) is treated
586
+ # as the budget for the whole run and split evenly across the N tests,
587
+ # rounded down, at least 1 -- a value set for serial runs shouldn't
588
+ # silently become N times as many threads. Serial runs leave the shell
589
+ # alone. Computed once, before anything changes ENV.
590
+ def test_thread_env
591
+ @test_thread_env ||= begin
592
+ n = options[:test_threads]
593
+ if n
594
+ raise Thor::Error, '--test-threads must be at least 1.' if n.to_i < 1
595
+
596
+ { 'OMP_NUM_THREADS' => n.to_i.to_s }
597
+ elsif jobs > 1
598
+ { 'OMP_NUM_THREADS' => split_threads.to_s }
599
+ else
600
+ {}
601
+ end
602
+ end
603
+ end
604
+
605
+ def split_threads
606
+ from_shell = ENV['OMP_NUM_THREADS'].to_i
607
+ budget, source = if from_shell.positive?
608
+ [from_shell, "OMP_NUM_THREADS=#{from_shell}"]
609
+ else
610
+ [Etc.nprocessors, "#{Etc.nprocessors} cores"]
611
+ end
612
+ each = [budget / jobs, 1].max
613
+ if budget < jobs
614
+ shell.say "--jobs #{jobs} is more than your #{source}; each test gets 1 thread " \
615
+ "(#{jobs} in all).", :yellow
616
+ else
617
+ idle = budget - each * jobs
618
+ @thread_note = "#{source} split #{jobs} ways#{"; #{idle} left idle" if idle.positive?}; " \
619
+ '--test-threads sets it directly'
620
+ end
621
+ each
622
+ end
623
+
624
+ def build_thread_env
625
+ n = options[:build_threads]
626
+ return {} if n.nil?
627
+ raise Thor::Error, '--build-threads must be at least 1.' if n.to_i < 1
628
+
629
+ { 'NPROCS' => n.to_i.to_s, 'OMP_NUM_THREADS' => n.to_i.to_s }
630
+ end
631
+
632
+ # Submit the results +t+ wrote during the run that began at +started+.
633
+ # If it wrote none (each_test_run died early, the run was killed), there
634
+ # is nothing to report: an older testhub.yml must not be sent as this
635
+ # run's result. Its claim is left to lapse, which tells the hub the work
636
+ # was abandoned. Returns whether a result went through.
637
+ def submit_result(s, m, t, started)
638
+ return false unless t.ran_since?(started)
639
+
640
+ s.submit_instance(m, t, force_logs: options['force-logs'], quiet: true)
641
+ end
642
+
643
+ # Check numeric options before anything is checked out or built, so a
644
+ # typo fails in a second rather than after a long compile.
645
+ def validate_options!
646
+ { jobs: 1, test_threads: 1, build_threads: 1 }.each do |name, min|
647
+ value = options[name]
648
+ next if value.nil?
649
+ next if value.is_a?(Numeric) && value == value.to_i && value >= min
650
+
651
+ raise Thor::Error, "--#{name.to_s.tr('_', '-')} must be a whole number of at least #{min}."
652
+ end
653
+ explicit_factor
654
+ nil
655
+ end
656
+
657
+ # Report this build's status (and its log, if it failed). Returns whether
658
+ # the report went through; false if there was nothing to report.
659
+ def submit_build(s, m)
660
+ unless m.install_attempted?
661
+ shell.say 'No build to report (the install never got far enough to ' \
662
+ 'write testhub.yml).', :red
663
+ return false
664
+ end
665
+ s.submit_commit(m, empty: true)
666
+ end
667
+
668
+ # The loaded tests of +mod+ (or every module), in do1_test_source order.
669
+ def tests_for(m, mod)
670
+ mods = mod == :all ? MesaTestCase.modules : [mod]
671
+ mods.flat_map do |md|
672
+ (m.test_case_names[md] || []).filter_map { |name| (m.test_cases[md] || {})[name] }
673
+ end
674
+ end
675
+
676
+ # Run every test in +mod+ (or all modules) on +m+'s build, in the current
677
+ # environment and MESA_DIR: one at a time (MESA's output on the terminal,
678
+ # as before), or --jobs at a time (output in each test's
679
+ # each_test_run.log). With +submit+, each test is claimed just as it
680
+ # starts and submitted as soon as it finishes, so a claim always means
681
+ # "running now" and results arrive continuously -- no bulk submission.
682
+ # Returns true if every submission went through.
683
+ def run_tests(s, m, mod:, submit:, flags: {})
684
+ m.check_installation
685
+ tests = tests_for(m, mod)
686
+ sha = m.sha
687
+ claiming = submit && options[:claim]
688
+ if jobs > 1
689
+ threads = test_thread_env['OMP_NUM_THREADS'] || ENV['OMP_NUM_THREADS']
690
+ say_step(shell, "Testing #{tests.size} tests, #{jobs} at a time" \
691
+ "#{", #{threads} thread#{'s' unless threads == '1'} each" if threads}")
692
+ shell.say "(#{@thread_note})" if @thread_note
693
+ shell.say "Each test's output goes to each_test_run.log in its directory."
694
+ end
695
+ lock = Mutex.new
696
+ done = 0
697
+ all_submitted = true
698
+ run_one = lambda do |t|
699
+ if claiming
700
+ lock.synchronize do
701
+ s.claim(sha: sha, scope: 'test', mod: t.mod, test_case: t.test_name, flags: flags, mesa: m)
702
+ end
703
+ end
704
+ started = Time.now
705
+ if jobs > 1
706
+ t.run_isolated
707
+ else
708
+ say_step(shell, "[#{lock.synchronize { done } + 1}/#{tests.size}] Testing #{t.mod}/#{t.test_name}")
709
+ t.do_one
710
+ end
711
+ lock.synchronize do
712
+ done += 1
713
+ # (not `&&=` on the call: that would skip every later submission)
714
+ submitted = finish_test(s, m, t, started, submit: submit, prefix: "[#{done}/#{tests.size}] ")
715
+ all_submitted &&= submitted
716
+ end
717
+ end
718
+ if jobs > 1
719
+ parallel_each(tests, jobs) { |t| run_one.call(t) }
720
+ else
721
+ tests.each { |t| run_one.call(t) }
722
+ end
723
+ all_submitted
724
+ end
725
+
726
+ # Submit a finished test (with +submit+) and print its one progress line:
727
+ # [12/107] star/1M_pre_ms_to_wd passed in 41.2 min, submitted
728
+ # Only the outcome is colored. Returns whether the result went in (true
729
+ # when not submitting).
730
+ def finish_test(s, m, t, started, submit:, prefix: '')
731
+ elapsed = format_duration(Time.now - started)
732
+ unless t.ran_since?(started)
733
+ line = "#{prefix}#{t.mod}/#{t.test_name} #{shell.set_color('wrote no results', :red)} " \
734
+ "after #{elapsed}"
735
+ shell.say(submit ? "#{line}; nothing submitted" : line)
736
+ return !submit
737
+ end
738
+
739
+ passed = begin
740
+ t.passed?
741
+ rescue StandardError
742
+ false
743
+ end
744
+ outcome = passed ? shell.set_color('passed', :green) : shell.set_color('FAILED', :red)
745
+ submitted = submit ? submit_result(s, m, t, started) : true
746
+ suffix = if !submit then ''
747
+ elsif submitted then ', submitted'
748
+ else ", #{shell.set_color('not submitted', :red)}"
749
+ end
750
+ shell.say "#{prefix}#{t.mod}/#{t.test_name} #{outcome} in #{elapsed}#{suffix}"
751
+ submitted
752
+ end
753
+
754
+ def format_duration(seconds)
755
+ if seconds < 90 then format('%d s', seconds.round)
756
+ elsif seconds < 2 * 3600 then format('%.1f min', seconds / 60.0)
757
+ else format('%.1f h', seconds / 3600.0)
758
+ end
759
+ end
760
+
761
+
762
+ # clean + install +m+ with +modes+ in the environment, then record the
763
+ # modes in $MESA_DIR/testhub.yml for later `test`/`submit` runs.
764
+ def build_with_modes(m, modes, raise_on_failure: false)
765
+ say_step(shell, "Building MESA #{short_sha(m.sha)}" \
766
+ "#{" (#{describe_modes(modes)})" unless modes.empty?}")
767
+ with_env(run_mode_env(modes).merge(build_thread_env)) do
768
+ m.clean
769
+ m.install
770
+ end
771
+ rescue MesaDirError
772
+ shell.say "MESA #{short_sha(m.sha)} failed to build; see #{File.join(m.mesa_dir, 'build.log')}.", :red
773
+ raise if raise_on_failure
774
+ else
775
+ # Recorded so later `mesa_test test` runs on this build use the same modes.
776
+ m.record_run_modes(modes)
777
+ shell.say "Built MESA #{short_sha(m.sha)}.", :green
778
+ end
779
+
780
+ # Run modes given explicitly on the command line (unset flags omitted).
781
+ def explicit_run_modes
782
+ modes = {}
783
+ modes[:skip_optional] = options[:skip_optional] unless options[:skip_optional].nil?
784
+ modes[:fpe] = options[:fpe] unless options[:fpe].nil?
785
+ modes[:resolution_factor] = (options[:converge] ? converge_factor : false) unless options[:converge].nil?
786
+ modes[:resolution_factor] = explicit_factor if explicit_factor
787
+ modes
788
+ end
789
+
790
+ # --resolution-factor, as a string for the environment; implies
791
+ # --converge, so it can't be combined with --no-converge.
792
+ def explicit_factor
793
+ factor = options[:resolution_factor]
794
+ return nil if factor.nil?
795
+ raise Thor::Error, '--resolution-factor must be positive.' unless factor.positive?
796
+ raise Thor::Error, '--resolution-factor implies --converge; drop --no-converge.' if options[:converge] == false
797
+
798
+ factor.to_s
799
+ end
800
+
801
+ # The run modes a dispatch asks for, with any --resolution-factor given
802
+ # here as the preferred converge factor (a factor the commit names
803
+ # still wins, with a warning).
804
+ def modes_for(rec)
805
+ if explicit_factor
806
+ dispatch_run_modes(rec, preferred: explicit_factor, preferred_source: '--resolution-factor')
807
+ else
808
+ dispatch_run_modes(rec)
809
+ end
810
+ end
811
+
812
+ # Modes for running tests on +m+'s build: what was recorded at install,
813
+ # overridden by flags given now — except FPE, which is compiled in.
814
+ def test_run_modes(m)
815
+ recorded = m.downloaded? ? m.recorded_run_modes : {}
816
+ explicit = explicit_run_modes
817
+ if recorded.key?(:fpe) && explicit.key?(:fpe) && explicit[:fpe] != recorded[:fpe]
818
+ shell.say "Ignoring --#{explicit[:fpe] ? '' : 'no-'}fpe: this build was compiled " \
819
+ "#{recorded[:fpe] ? 'with' : 'without'} FPE checks.", :yellow
820
+ explicit.delete(:fpe)
821
+ end
822
+ # A factor the build recorded (e.g. from `[ci converge 0.5]` via
823
+ # `install best`) beats the shell's; say so when they differ.
824
+ shell_factor = ENV['MESA_TEST_SUITE_RESOLUTION_FACTOR']
825
+ if recorded[:resolution_factor] && !explicit.key?(:resolution_factor) && shell_factor &&
826
+ shell_factor.to_f != recorded[:resolution_factor].to_f
827
+ warn_yellow "This build was set up for resolution factor #{recorded[:resolution_factor]}; " \
828
+ "using it instead of your MESA_TEST_SUITE_RESOLUTION_FACTOR=#{shell_factor}."
829
+ end
830
+ recorded.merge(explicit)
831
+ end
832
+
833
+ # What a claim says the work will run with (MESATestHub's use_* flags).
834
+ def claim_flags(modes)
835
+ {
836
+ fpe: modes[:fpe] == true,
837
+ full_inlists: modes[:skip_optional] == false,
838
+ converge: modes[:resolution_factor] ? true : false
839
+ }
840
+ end
841
+
842
+ def describe_modes(modes)
843
+ modes.map do |k, v|
844
+ case k
845
+ when :fpe then v ? 'FPE checks on' : 'FPE checks off'
846
+ when :skip_optional then v ? 'skip optional inlists' : 'run optional inlists'
847
+ when :resolution_factor then v ? "resolution factor #{v}" : 'default resolution'
848
+ end
849
+ end.join(', ')
850
+ end
851
+
852
+ # Optional modes this computer is willing to run for MESATestHub: the
853
+ # config file's `capabilities`, overridden by any mode flag given.
854
+ def capabilities_for(s)
855
+ caps = s.capabilities.dup
856
+ caps[:fpe] = options[:fpe] unless options[:fpe].nil?
857
+ caps[:full_inlists] = !options[:skip_optional] unless options[:skip_optional].nil?
858
+ caps[:converge] = options[:converge] unless options[:converge].nil?
859
+ caps[:converge] = true if options[:resolution_factor]
860
+ caps
861
+ end
862
+
863
+ # Ask for a commit to build, exiting with NOTHING_TO_DO_EXIT if there is
864
+ # none — before anything touches the work directory.
865
+ def dispatch_build_or_exit(s)
866
+ dispatch_or_exit(s, scope: 'build') || exit(NOTHING_TO_DO_EXIT)
867
+ end
868
+
869
+ # Dispatch, or print the server's complaint and exit 1. nil means there's
870
+ # nothing to do (said on STDERR so `request_work` STDOUT stays pure JSON).
871
+ def dispatch_or_exit(s, scope:, sha: nil, capabilities: capabilities_for(s))
872
+ rec = s.dispatch(scope: scope, sha: sha, capabilities: capabilities)
873
+ if rec.nil?
874
+ $stderr.puts 'MESATestHub has nothing for this computer to ' \
875
+ "#{scope == 'build' ? 'build' : 'test on this commit'} right now."
876
+ elsif scope == 'build'
877
+ # (the hub's first reason is usually "on <branch>", already said)
878
+ reasons = Array(rec['reasons']) - ["on #{rec['branch']}"]
879
+ $stderr.puts "MESATestHub picked #{rec['short_sha']} on #{rec['branch']}: #{reasons.join('; ')}"
880
+ end
881
+ rec
882
+ rescue TestHubError => e
883
+ $stderr.puts e.message
884
+ exit 1
885
+ end
886
+
369
887
  def emit_search_response(response)
370
888
  if response.is_a?(Net::HTTPSuccess)
371
889
  puts response.body
@@ -374,6 +892,10 @@ class MesaTest < Thor
374
892
  $stderr.puts "Search request failed: HTTP #{response.code} "\
375
893
  "#{response.message}"
376
894
  $stderr.puts response.body unless response.body.nil? || response.body.empty?
895
+ if response.code == '401'
896
+ $stderr.puts "\nCheck your API key (`mesa_test setup`, or MESATESTHUB_API_KEY).\n" \
897
+ "#{MesaTestSubmitter::API_KEY_HELP}"
898
+ end
377
899
  exit 1
378
900
  end
379
901
  end
@@ -404,11 +926,16 @@ class MesaTest < Thor
404
926
  puts computer_check['message']
405
927
  else
406
928
  $stderr.puts computer_check['message']
929
+ unless computer_check['reachable'] == false
930
+ $stderr.puts 'Check your API key (`mesa_test setup`, or MESATESTHUB_API_KEY).'
931
+ end
407
932
  exit 1
408
933
  end
409
934
  end
410
935
 
411
- def create_submitter(force: true)
936
+ # hub: true for commands that talk to MESATestHub, which all need an API
937
+ # key; checked first, before any checkout or build.
938
+ def create_submitter(force: true, hub: false)
412
939
  s = case MODE
413
940
  when :development
414
941
  MesaTestSubmitter.new_from_config(base_uri: DEVELOPMENT_URI)
@@ -420,11 +947,12 @@ class MesaTest < Thor
420
947
  unless force
421
948
  s.setup unless s.confirm_computer_data
422
949
  end
950
+ s.require_api_key! if hub
423
951
  s
424
952
  end
425
953
 
426
954
  def create_and_check_submitter(force: true)
427
- s = create_submitter(force: force)
955
+ s = create_submitter(force: force, hub: true)
428
956
  check_user_and_computer s
429
957
  s
430
958
  end
@@ -467,4 +995,13 @@ class MesaTest < Thor
467
995
  end
468
996
 
469
997
  # actually start the CLI
470
- MesaTest.start(ARGV)
998
+ begin
999
+ MesaTest.start(ARGV)
1000
+ rescue Interrupt
1001
+ # run_workers has already stopped any tests running in parallel.
1002
+ $stderr.puts "\nInterrupted."
1003
+ exit 130
1004
+ rescue TestHubError => e
1005
+ $stderr.puts e.message
1006
+ exit 1
1007
+ end