mesa_test 1.2.0 → 2.0.0.rc1

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 +644 -126
  3. data/lib/mesa_test.rb +685 -179
  4. metadata +7 -6
data/bin/mesa_test CHANGED
@@ -15,26 +15,97 @@ 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
@@ -44,8 +115,12 @@ class MesaTest < Thor
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,30 +132,35 @@ 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
+ # run test
149
+ started = Time.now
150
+ t.do_one
151
+
152
+ # bail out if not submitting
153
+ return unless options[:submit]
154
+
155
+ # submit results
156
+ shell.say 'Submitting results to ' + s.base_uri + '... ', :blue
157
+ submit_result(s, m, t, started)
158
+ else
159
+ # Each test is claimed as it starts and submitted as it finishes.
160
+ # (`install` reports the build.)
161
+ run_tests(s, m, mod: options[:module].downcase.to_sym,
162
+ submit: options[:submit], flags: claim_flags(modes))
163
+ end
84
164
  end
85
165
  end
86
166
  shell.say "Done.\n", :green
@@ -88,27 +168,28 @@ class MesaTest < Thor
88
168
 
89
169
  desc 'submit [TEST CASE]', 'submit a test case or commit'
90
170
  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:
171
+ Submits results already in your work directory (mesa_work in
172
+ ~/.mesa_test/config.yml, or --mesadir) without running anything: say, from
173
+ a cluster's head node after jobs ran with --no-submit, or to retry after a
174
+ network failure. With TEST_CASE (a name or number, as for `test`), just
175
+ that test's result. Without it, the build status plus every test with
176
+ results from this build (or with --empty, just the build status), in one
177
+ request. Results left over from an earlier build are skipped, and
178
+ MesaTestHub ignores results it already has, so submitting again is safe.
179
+ Needs an API key (see `mesa_test help setup`). Modules are
180
+ searched/concatenated in this order:
97
181
 
98
182
  #{MesaTestCase.modules.map { |mod| ' ' + mod.to_s }.join(', ')}
99
183
  LONGDESC
100
184
 
101
- option :force, desc: 'When submitting, assume submission data in ' \
102
- '~/.mesa_test.yml are correct rather than checking ' \
103
- 'with user.',
185
+ option :force, desc: 'Use ~/.mesa_test/config.yml as it is. --no-force ' \
186
+ 'shows it and asks you to confirm (or fix) it first.',
104
187
  type: :boolean, default: true, aliases: '-f'
105
188
  option 'force-logs', desc: 'Submit logs for test case(s), even if they pass',
106
189
  type: :boolean, default: false
107
190
  option :module, desc: 'Which module to test and/or submit, if applicable.',
108
191
  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.',
192
+ option :empty, desc: 'Submit only the build status, no test results.',
112
193
  type: :boolean, default: false, aliases: '-e'
113
194
  option :mesadir, desc: 'Location of mesa dir, if not relying on config data',
114
195
  type: :string
@@ -133,13 +214,17 @@ class MesaTest < Thor
133
214
 
134
215
  t = create_and_check_test_case(mesa: m, test_case_name: test_case_name,
135
216
  mod: options[:module].downcase.to_sym)
217
+ raise Thor::Error, "#{t.mod}/#{t.test_name} has no results to submit." unless t.ran?
218
+
136
219
  # submit results
137
220
  shell.say 'Submitting results to ' + s.base_uri + '... ', :blue
138
221
  s.submit_instance(m, t, force_logs: options['force-logs'])
139
222
  shell.say "Done.\n", :green
140
223
  else
141
- # submitting compilation information and either all tests (entire) or
142
- # no tests (empty)
224
+ # The build status plus (unless --empty) every test with results from
225
+ # this build, in one request. MESATestHub stores it as a build row
226
+ # plus one row per test and ignores anything it already has (by
227
+ # fingerprint), so re-running `submit` is safe.
143
228
  begin
144
229
  m.check_installation
145
230
  rescue MesaDirError
@@ -147,22 +232,18 @@ class MesaTest < Thor
147
232
  'properly. Attempting to submit a compilation failure to '\
148
233
  'MESATestHub.', :red
149
234
  empty = true
150
- ensure
151
- # submit all tests
152
- s.submit_commit(m, empty: empty, force_logs: options['force-logs'])
153
235
  end
236
+ s.submit_commit(m, empty: empty, force_logs: options['force-logs'])
154
237
  end
155
238
  end
156
239
  end
157
240
 
158
241
  desc 'checkout [SHA]', 'checkout a commit'
159
242
  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).
243
+ Checks out SHA (default: main) into your work directory (mesa_work in
244
+ ~/.mesa_test/config.yml, or --mesadir), using git worktrees off the mirror
245
+ so MESA's history isn't downloaded again. Doesn't build, doesn't contact
246
+ MesaTestHub, and doesn't touch your own MESA_DIR or environment.
166
247
  LONGDESC
167
248
 
168
249
  option :mesadir, desc: 'Location of mesa dir, if not relying on config data',
@@ -180,16 +261,30 @@ class MesaTest < Thor
180
261
 
181
262
  desc 'install [SHA]', 'checkout and install a commit'
182
263
  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
264
+ Checks out SHA (default: main) into your work directory (mesa_work in
265
+ ~/.mesa_test/config.yml, or --mesadir) and builds it: ./clean and
266
+ ./install, with git worktrees off the mirror so MESA's history isn't
267
+ downloaded again. The SDK or compilers must already be set up. Doesn't
268
+ touch your own MESA_DIR or environment. With --no-checkout, builds what's
269
+ already there and ignores SHA.
270
+
271
+ The build is claimed and its result (pass or fail, with the build log)
272
+ reported to MesaTestHub, which needs an API key (see `mesa_test help
273
+ setup`). A failed build exits with status 1. --no-submit just builds, for
274
+ local work: nothing is claimed or sent, and no key is needed.
275
+
276
+ Run modes given here (--skip-optional, --fpe, --converge) are used for
277
+ the build and recorded in $MESA_DIR/testhub.yml, so later `mesa_test test`
278
+ runs on this build (e.g. cluster array jobs) apply them automatically.
279
+
280
+ If SHA is `best`, MesaTestHub picks the commit this computer should build
281
+ next, and the run modes come from its request instead: the mode flags then
282
+ say what this computer is *willing* to do (overriding `capabilities` in the
283
+ config file). If nothing needs building, mesa_test exits with status
284
+ #{NOTHING_TO_DO_EXIT} without touching the work directory, so a script can
285
+ stop:
286
+
287
+ mesa_test install best || exit
193
288
  LONGDESC
194
289
 
195
290
  option :checkout, desc: 'Whether a checkout needs to happen. Use '\
@@ -197,12 +292,30 @@ class MesaTest < Thor
197
292
  type: :boolean, default: true, aliases: '-c'
198
293
  option :mesadir, desc: 'Location of mesa dir, if not relying on config data',
199
294
  type: :string
295
+ option :submit, desc: 'Report the build (claim it first, then submit its ' \
296
+ 'status) to MesaTestHub. --no-submit just builds, ' \
297
+ 'e.g. for local testing.',
298
+ type: :boolean, default: true
299
+ claim_option
300
+ run_mode_options
301
+ build_threads_option
200
302
 
201
303
  def install(sha = 'main')
304
+ validate_options!
202
305
  # assuming config wizard has already been set (perhaps a little reckless)
203
306
  # so that we can get at the work directory and mirror directory
204
- s = create_submitter(force: true)
205
- m = if options[:checkout]
307
+ s = if options[:submit] || sha == 'best'
308
+ create_and_check_submitter(force: true)
309
+ else
310
+ create_submitter(force: true)
311
+ end
312
+ rec = nil
313
+ if sha == 'best'
314
+ rec = dispatch_build_or_exit(s)
315
+ sha = rec['commit_sha']
316
+ end
317
+ modes = rec ? modes_for(rec) : explicit_run_modes
318
+ m = if options[:checkout] || rec
206
319
  Mesa.checkout(
207
320
  sha: sha,
208
321
  work_dir: work_dir(options[:mesadir], s.mesa_work),
@@ -217,36 +330,51 @@ class MesaTest < Thor
217
330
  github_protocol: s.github_protocol
218
331
  )
219
332
  end
220
- begin
221
- m.clean
222
- m.install
333
+ if options[:submit] && options[:claim]
334
+ s.claim(sha: m.sha, scope: 'build', flags: claim_flags(modes),
335
+ dispatched_at: rec && rec['dispatched_at'])
336
+ end
337
+ built = begin
338
+ build_with_modes(m, modes, raise_on_failure: true)
339
+ true
223
340
  rescue MesaDirError
224
- shell.say "\nFailed in compiling MESA.", :red
225
- else
226
- shell.say "\nSuccessfully compiled MESA commit #{m.sha}.", :green
341
+ false
227
342
  end
343
+ # Report the build either way (a failure carries its build log), like
344
+ # `test` reports each test.
345
+ submit_build(s, m) if options[:submit]
346
+ exit 1 unless built
228
347
  end
229
348
 
230
349
  desc 'install_and_test [SHA]', 'Install, test, and submit an entire commit.'
231
350
  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
-
351
+ Checks out SHA (default: main) into your work directory (mesa_work in
352
+ ~/.mesa_test/config.yml, or --mesadir), builds it (./clean and ./install;
353
+ the SDK or compilers must already be set up), and runs the test suite.
354
+ Doesn't touch your own MESA_DIR or environment. Roughly a shortcut for
355
+
356
+ mesa_test install SHA && mesa_test test
357
+
358
+ The build status is submitted as soon as the build finishes (a failed
359
+ build is reported and exits 1), then each test is claimed as it starts
360
+ and submitted as it finishes, so results roll in during the run. This
361
+ needs an API key (see `mesa_test help setup`). --no-submit builds and
362
+ tests locally without claiming or sending anything. With --no-checkout,
363
+ the build in the work directory is used and SHA is ignored.
364
+
365
+ --jobs N runs N tests at once; see also --test-threads and --build-threads.
366
+
367
+ Run modes (--skip-optional, --fpe, --converge) apply to the build and
368
+ every test; unset ones are left to your shell's environment.
369
+
370
+ If SHA is `best`, MesaTestHub picks the commit (one this computer hasn't
371
+ tested yet) and the run modes (FPE checks, plus full inlists or a
372
+ convergence run if the commit asks for one); everything else is the
373
+ ordinary run above. The mode flags then say what this computer is
374
+ willing to do (overriding `capabilities` in the config file): commits
375
+ asking for [ci optional] or [ci converge] get those runs only if it's
376
+ willing. --no-submit doesn't apply in this mode. If nothing needs
377
+ testing, mesa_test exits with status #{NOTHING_TO_DO_EXIT}.
250
378
  LONGDESC
251
379
 
252
380
  option :checkout, desc: 'Whether a checkout needs to happen. Use '\
@@ -254,9 +382,8 @@ class MesaTest < Thor
254
382
  type: :boolean, default: true, aliases: '-c'
255
383
  option :mesadir, desc: 'Location of mesa dir, if not relying on config data',
256
384
  type: :string
257
- option :force, desc: 'When submitting, assume submission data in ' \
258
- '~/.mesa_test.yml are correct rather than checking ' \
259
- 'with user.',
385
+ option :force, desc: 'Use ~/.mesa_test/config.yml as it is. --no-force ' \
386
+ 'shows it and asks you to confirm (or fix) it first.',
260
387
  type: :boolean, default: true, aliases: '-f'
261
388
  option :submit, desc: 'Submit results (if any) to MesaTestHub.',
262
389
  type: :boolean, default: true
@@ -264,8 +391,15 @@ class MesaTest < Thor
264
391
  type: :string, default: 'all', aliases: '-m'
265
392
  option :destroy, type: :boolean, desc: 'Delete mesa installation upon ' \
266
393
  'submission of complete results.', default: false
394
+ claim_option
395
+ run_mode_options
396
+ build_threads_option
397
+ parallel_options
267
398
 
268
399
  def install_and_test(sha='main')
400
+ validate_options!
401
+ return install_and_test_best if sha == 'best'
402
+
269
403
  s = if options[:submit]
270
404
  create_and_check_submitter(force: options[:force])
271
405
  else
@@ -290,38 +424,80 @@ class MesaTest < Thor
290
424
  )
291
425
  end
292
426
 
427
+ modes = explicit_run_modes
428
+ claiming = options[:submit] && options[:claim]
429
+ s.claim(sha: m.sha, scope: 'build', flags: claim_flags(modes)) if claiming
430
+
293
431
  begin
294
- m.clean
295
- m.install
432
+ build_with_modes(m, modes, raise_on_failure: true)
296
433
  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'])
434
+ # Report the failed build (with its log) right away.
435
+ submit_build(s, m) if options[:submit]
436
+ exit 1
437
+ end
438
+
439
+ check_mesa(m)
440
+ update_mesa(mesa: m, mod: options[:module].downcase.to_sym)
441
+ # The build's status goes in as soon as it's known, then each test as
442
+ # soon as it finishes -- results roll in rather than arriving in one
443
+ # bulk submission at the end.
444
+ build_submitted = options[:submit] ? submit_build(s, m) : true
445
+ all_submitted = with_env(run_mode_env(m.recorded_run_modes).merge(test_thread_env)) do
446
+ m.with_mesa_dir do
447
+ run_tests(s, m, mod: options[:module].downcase.to_sym,
448
+ submit: options[:submit], flags: claim_flags(modes))
311
449
  end
450
+ end
312
451
 
313
- # if requested, installation and submission are successful, destroy the
314
- # directory
315
- m.remove if successfully_submitted && options[:destroy] && m.installed?
452
+ # if requested, installation and submission are successful, destroy the
453
+ # directory
454
+ if options[:submit] && build_submitted && all_submitted && options[:destroy] && m.installed?
455
+ m.remove
316
456
  end
317
457
  end
318
458
 
319
- desc 'setup [CONFIG_FILE]', 'Setup MesaTestHub config file.'
459
+ desc 'request_work', 'ask MesaTestHub what this computer should do next'
460
+ long_desc <<-LONGDESC
461
+ Asks MesaTestHub which commit this computer should build next (or, with
462
+ --scope=test --sha=SHA, which test to run next on a commit it built) and
463
+ prints the recommendation as JSON on STDOUT, including the run modes to
464
+ use and the reasons it was picked. The mode flags say what this computer
465
+ is willing to do (overriding `capabilities` in the config file). Prints
466
+ nothing and exits with status #{NOTHING_TO_DO_EXIT} when there is nothing
467
+ useful to do. Read-only: nothing is claimed. Needs an API key (see
468
+ `mesa_test help setup`).
469
+ LONGDESC
470
+
471
+ option :scope, desc: 'build or test', type: :string, default: 'build',
472
+ enum: %w[build test]
473
+ option :sha, desc: 'For --scope=test: the commit you built', type: :string
474
+ run_mode_options
475
+
476
+ def request_work
477
+ s = create_submitter(force: true, hub: true)
478
+ rec = dispatch_or_exit(s, scope: options[:scope], sha: options[:sha])
479
+ exit NOTHING_TO_DO_EXIT unless rec
480
+
481
+ puts JSON.pretty_generate(rec)
482
+ end
483
+
484
+ desc 'setup [CONFIG_FILE]', 'Set up this computer (API key, test modes, directories)'
320
485
  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.
486
+ Walks through this computer's settings, offering the current ones as
487
+ defaults, and saves them to CONFIG_FILE (default ~/.mesa_test/config.yml,
488
+ which is where the other commands look).
489
+
490
+ mesa_test authenticates with a per-computer API key. To get one, log in
491
+ to MesaTestHub, open My Computers, open this computer's page (adding the
492
+ computer first if needed; one used only for `search` and `count` is
493
+ fine), and click "Generate key". The key starts with mth_. Paste it when
494
+ the wizard asks, or export MESATESTHUB_API_KEY, which overrides the
495
+ config file (e.g. to keep the key off disk on a cluster).
496
+
497
+ The wizard checks the key with MesaTestHub, which also supplies this
498
+ computer's name. Then it asks which optional test modes the computer is
499
+ willing to run when a commit asks for them (full_inlists, fpe, converge),
500
+ the logs token, and where the MESA mirror and work directory live.
325
501
  LONGDESC
326
502
  def setup(config_file = File.join(ENV['HOME'], '.mesa_test', 'config.yml'))
327
503
  MesaTestSubmitter.new_from_config(config_file: config_file,
@@ -331,8 +507,8 @@ class MesaTest < Thor
331
507
  desc 'search "QUERY"', 'retrieve JSON test instances matching QUERY'
332
508
  long_desc <<-LONGDESC
333
509
  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).
510
+ quoted. Authenticates with your API key; a key for any of your computers
511
+ works, since search is read-only.
336
512
 
337
513
  Query syntax is the same key-value form used by the in-browser search box
338
514
  at testhub.mesastar.org/test_instances/search (see the "Query syntax"
@@ -348,7 +524,7 @@ class MesaTest < Thor
348
524
  LONGDESC
349
525
 
350
526
  def search(query)
351
- s = create_submitter(force: true)
527
+ s = create_submitter(force: true, hub: true)
352
528
  emit_search_response(s.search(query))
353
529
  end
354
530
 
@@ -360,12 +536,335 @@ class MesaTest < Thor
360
536
  LONGDESC
361
537
 
362
538
  def count(query)
363
- s = create_submitter(force: true)
539
+ s = create_submitter(force: true, hub: true)
364
540
  emit_search_response(s.search_count(query))
365
541
  end
366
542
 
367
543
  private
368
544
 
545
+ # `install_and_test best`: MesaTestHub picks the commit, then each test.
546
+ def install_and_test_best
547
+ unless options[:submit]
548
+ raise Thor::Error, '`install_and_test best` submits as it goes; ' \
549
+ '--no-submit is not supported.'
550
+ end
551
+ s = create_and_check_submitter(force: options[:force])
552
+ rec = dispatch_build_or_exit(s)
553
+ modes = modes_for(rec)
554
+ m = Mesa.checkout(sha: rec['commit_sha'],
555
+ work_dir: work_dir(options[:mesadir], s.mesa_work),
556
+ mirror_dir: s.mesa_mirror,
557
+ github_protocol: s.github_protocol)
558
+ if options[:claim]
559
+ s.claim(sha: m.sha, scope: 'build', flags: claim_flags(modes),
560
+ dispatched_at: rec['dispatched_at'])
561
+ end
562
+
563
+ begin
564
+ build_with_modes(m, modes, raise_on_failure: true)
565
+ rescue MesaDirError
566
+ submit_build(s, m)
567
+ exit 1
568
+ end
569
+ mod = options[:module].downcase.to_sym
570
+ check_mesa(m)
571
+ update_mesa(mesa: m, mod: mod)
572
+
573
+ # Report the build on its own; this also fulfills the build claim.
574
+ build_submitted = submit_build(s, m)
575
+ # MesaTestHub chose the commit and its modes; from here it's an ordinary
576
+ # whole-suite run, as `install_and_test SHA` does it: MESA's order,
577
+ # --jobs at a time, each test claimed as it starts and submitted as it
578
+ # finishes.
579
+ all_submitted = with_env(run_mode_env(m.recorded_run_modes).merge(test_thread_env)) do
580
+ m.with_mesa_dir do
581
+ run_tests(s, m, mod: mod, submit: true, flags: claim_flags(modes))
582
+ end
583
+ end
584
+ m.remove if build_submitted && all_submitted && options[:destroy] && m.installed?
585
+ end
586
+
587
+
588
+ def jobs
589
+ j = options[:jobs].to_i
590
+ raise Thor::Error, '--jobs must be at least 1.' if j < 1
591
+
592
+ j
593
+ end
594
+
595
+ # OMP_NUM_THREADS for each test. --test-threads if given. Otherwise, with
596
+ # --jobs N, the shell's OMP_NUM_THREADS (else the core count) is treated
597
+ # as the budget for the whole run and split evenly across the N tests,
598
+ # rounded down, at least 1 -- a value set for serial runs shouldn't
599
+ # silently become N times as many threads. Serial runs leave the shell
600
+ # alone. Computed once, before anything changes ENV.
601
+ def test_thread_env
602
+ @test_thread_env ||= begin
603
+ n = options[:test_threads]
604
+ if n
605
+ raise Thor::Error, '--test-threads must be at least 1.' if n.to_i < 1
606
+
607
+ { 'OMP_NUM_THREADS' => n.to_i.to_s }
608
+ elsif jobs > 1
609
+ { 'OMP_NUM_THREADS' => split_threads.to_s }
610
+ else
611
+ {}
612
+ end
613
+ end
614
+ end
615
+
616
+ def split_threads
617
+ from_shell = ENV['OMP_NUM_THREADS'].to_i
618
+ budget, source = if from_shell.positive?
619
+ [from_shell, "OMP_NUM_THREADS=#{from_shell}"]
620
+ else
621
+ [Etc.nprocessors, "#{Etc.nprocessors} cores"]
622
+ end
623
+ each = [budget / jobs, 1].max
624
+ if budget < jobs
625
+ shell.say "--jobs #{jobs} is more than your #{source}; each test gets 1 thread " \
626
+ "(#{jobs} in all).", :yellow
627
+ else
628
+ idle = budget - each * jobs
629
+ note = idle.positive? ? "; #{idle} left idle" : ''
630
+ shell.say "Up to #{jobs} tests at a time, #{each} thread#{'s' unless each == 1} each " \
631
+ "(#{source} ÷ #{jobs}#{note}). Use --test-threads to set it yourself.", :blue
632
+ end
633
+ each
634
+ end
635
+
636
+ def build_thread_env
637
+ n = options[:build_threads]
638
+ return {} if n.nil?
639
+ raise Thor::Error, '--build-threads must be at least 1.' if n.to_i < 1
640
+
641
+ { 'NPROCS' => n.to_i.to_s, 'OMP_NUM_THREADS' => n.to_i.to_s }
642
+ end
643
+
644
+ # Submit the results +t+ wrote during the run that began at +started+.
645
+ # If it wrote none (each_test_run died early, the run was killed), there
646
+ # is nothing to report: an older testhub.yml must not be sent as this
647
+ # run's result. Its claim is left to lapse, which tells the hub the work
648
+ # was abandoned. Returns whether a result went through.
649
+ def submit_result(s, m, t, started)
650
+ unless t.ran_since?(started)
651
+ shell.say "#{t.mod}/#{t.test_name} wrote no results; nothing submitted.", :red
652
+ return false
653
+ end
654
+ s.submit_instance(m, t, force_logs: options['force-logs'])
655
+ end
656
+
657
+ # Check numeric options before anything is checked out or built, so a
658
+ # typo fails in a second rather than after a long compile.
659
+ def validate_options!
660
+ { jobs: 1, test_threads: 1, build_threads: 1 }.each do |name, min|
661
+ value = options[name]
662
+ next if value.nil?
663
+ next if value.is_a?(Numeric) && value == value.to_i && value >= min
664
+
665
+ raise Thor::Error, "--#{name.to_s.tr('_', '-')} must be a whole number of at least #{min}."
666
+ end
667
+ explicit_factor
668
+ nil
669
+ end
670
+
671
+ # Report this build's status (and its log, if it failed). Returns whether
672
+ # the report went through; false if there was nothing to report.
673
+ def submit_build(s, m)
674
+ unless m.install_attempted?
675
+ shell.say 'No build to report (the install never got far enough to ' \
676
+ 'write testhub.yml).', :red
677
+ return false
678
+ end
679
+ s.submit_commit(m, empty: true)
680
+ end
681
+
682
+ # The loaded tests of +mod+ (or every module), in do1_test_source order.
683
+ def tests_for(m, mod)
684
+ mods = mod == :all ? MesaTestCase.modules : [mod]
685
+ mods.flat_map do |md|
686
+ (m.test_case_names[md] || []).filter_map { |name| (m.test_cases[md] || {})[name] }
687
+ end
688
+ end
689
+
690
+ # Run every test in +mod+ (or all modules) on +m+'s build, in the current
691
+ # environment and MESA_DIR: one at a time (MESA's output on the terminal,
692
+ # as before), or --jobs at a time (output in each test's
693
+ # each_test_run.log). With +submit+, each test is claimed just as it
694
+ # starts and submitted as soon as it finishes, so a claim always means
695
+ # "running now" and results arrive continuously -- no bulk submission.
696
+ # Returns true if every submission went through.
697
+ def run_tests(s, m, mod:, submit:, flags: {})
698
+ m.check_installation
699
+ tests = tests_for(m, mod)
700
+ sha = m.sha
701
+ claiming = submit && options[:claim]
702
+ shell.say "Running #{tests.size} tests, #{jobs} at a time.", :blue if jobs > 1
703
+ lock = Mutex.new
704
+ done = 0
705
+ all_submitted = true
706
+ run_one = lambda do |t|
707
+ if claiming
708
+ lock.synchronize do
709
+ s.claim(sha: sha, scope: 'test', mod: t.mod, test_case: t.test_name, flags: flags, mesa: m)
710
+ end
711
+ end
712
+ started = Time.now
713
+ jobs > 1 ? t.run_isolated : t.do_one
714
+ lock.synchronize do
715
+ done += 1
716
+ report_test(t, started, "[#{done}/#{tests.size}]")
717
+ if submit
718
+ # (not `&&=` on the call: that would skip every later submission)
719
+ submitted = submit_result(s, m, t, started)
720
+ all_submitted &&= submitted
721
+ end
722
+ end
723
+ end
724
+ if jobs > 1
725
+ parallel_each(tests, jobs) { |t| run_one.call(t) }
726
+ else
727
+ tests.each { |t| run_one.call(t) }
728
+ end
729
+ all_submitted
730
+ end
731
+
732
+ # One progress line for a finished test, from its testhub.yml.
733
+ def report_test(t, started, prefix)
734
+ outcome = begin
735
+ t.ran? ? (t.passed? ? 'pass' : 'FAIL') : 'no result'
736
+ rescue StandardError
737
+ 'no result'
738
+ end
739
+ shell.say format('%s %s/%s: %s (%.1f min)', prefix, t.mod, t.test_name, outcome,
740
+ (Time.now - started) / 60.0), outcome == 'pass' ? :green : :red
741
+ end
742
+
743
+
744
+ # clean + install +m+ with +modes+ in the environment, then record the
745
+ # modes in $MESA_DIR/testhub.yml for later `test`/`submit` runs.
746
+ def build_with_modes(m, modes, raise_on_failure: false)
747
+ with_env(run_mode_env(modes).merge(build_thread_env)) do
748
+ m.clean
749
+ m.install
750
+ end
751
+ rescue MesaDirError
752
+ shell.say "\nFailed in compiling MESA.", :red
753
+ raise if raise_on_failure
754
+ else
755
+ m.record_run_modes(modes)
756
+ shell.say "\nSuccessfully compiled MESA commit #{m.sha}.", :green
757
+ unless modes.empty?
758
+ shell.say "Recorded run modes for this build: #{describe_modes(modes)}.", :blue
759
+ end
760
+ end
761
+
762
+ # Run modes given explicitly on the command line (unset flags omitted).
763
+ def explicit_run_modes
764
+ modes = {}
765
+ modes[:skip_optional] = options[:skip_optional] unless options[:skip_optional].nil?
766
+ modes[:fpe] = options[:fpe] unless options[:fpe].nil?
767
+ modes[:resolution_factor] = (options[:converge] ? converge_factor : false) unless options[:converge].nil?
768
+ modes[:resolution_factor] = explicit_factor if explicit_factor
769
+ modes
770
+ end
771
+
772
+ # --resolution-factor, as a string for the environment; implies
773
+ # --converge, so it can't be combined with --no-converge.
774
+ def explicit_factor
775
+ factor = options[:resolution_factor]
776
+ return nil if factor.nil?
777
+ raise Thor::Error, '--resolution-factor must be positive.' unless factor.positive?
778
+ raise Thor::Error, '--resolution-factor implies --converge; drop --no-converge.' if options[:converge] == false
779
+
780
+ factor.to_s
781
+ end
782
+
783
+ # The run modes a dispatch asks for, with any --resolution-factor given
784
+ # here as the preferred converge factor (a factor the commit names
785
+ # still wins, with a warning).
786
+ def modes_for(rec)
787
+ if explicit_factor
788
+ dispatch_run_modes(rec, preferred: explicit_factor, preferred_source: '--resolution-factor')
789
+ else
790
+ dispatch_run_modes(rec)
791
+ end
792
+ end
793
+
794
+ # Modes for running tests on +m+'s build: what was recorded at install,
795
+ # overridden by flags given now — except FPE, which is compiled in.
796
+ def test_run_modes(m)
797
+ recorded = m.downloaded? ? m.recorded_run_modes : {}
798
+ explicit = explicit_run_modes
799
+ if recorded.key?(:fpe) && explicit.key?(:fpe) && explicit[:fpe] != recorded[:fpe]
800
+ shell.say "Ignoring --#{explicit[:fpe] ? '' : 'no-'}fpe: this build was compiled " \
801
+ "#{recorded[:fpe] ? 'with' : 'without'} FPE checks.", :yellow
802
+ explicit.delete(:fpe)
803
+ end
804
+ # A factor the build recorded (e.g. from `[ci converge 0.5]` via
805
+ # `install best`) beats the shell's; say so when they differ.
806
+ shell_factor = ENV['MESA_TEST_SUITE_RESOLUTION_FACTOR']
807
+ if recorded[:resolution_factor] && !explicit.key?(:resolution_factor) && shell_factor &&
808
+ shell_factor.to_f != recorded[:resolution_factor].to_f
809
+ warn "This build was set up for resolution factor #{recorded[:resolution_factor]}; " \
810
+ "using it instead of your MESA_TEST_SUITE_RESOLUTION_FACTOR=#{shell_factor}."
811
+ end
812
+ recorded.merge(explicit)
813
+ end
814
+
815
+ # What a claim says the work will run with (MESATestHub's use_* flags).
816
+ def claim_flags(modes)
817
+ {
818
+ fpe: modes[:fpe] == true,
819
+ full_inlists: modes[:skip_optional] == false,
820
+ converge: modes[:resolution_factor] ? true : false
821
+ }
822
+ end
823
+
824
+ def describe_modes(modes)
825
+ modes.map do |k, v|
826
+ case k
827
+ when :fpe then v ? 'FPE checks on' : 'FPE checks off'
828
+ when :skip_optional then v ? 'skip optional inlists' : 'run optional inlists'
829
+ when :resolution_factor then v ? "resolution factor #{v}" : 'default resolution'
830
+ end
831
+ end.join(', ')
832
+ end
833
+
834
+ # Optional modes this computer is willing to run for MesaTestHub: the
835
+ # config file's `capabilities`, overridden by any mode flag given.
836
+ def capabilities_for(s)
837
+ caps = s.capabilities.dup
838
+ caps[:fpe] = options[:fpe] unless options[:fpe].nil?
839
+ caps[:full_inlists] = !options[:skip_optional] unless options[:skip_optional].nil?
840
+ caps[:converge] = options[:converge] unless options[:converge].nil?
841
+ caps[:converge] = true if options[:resolution_factor]
842
+ caps
843
+ end
844
+
845
+ # Ask for a commit to build, exiting with NOTHING_TO_DO_EXIT if there is
846
+ # none — before anything touches the work directory.
847
+ def dispatch_build_or_exit(s)
848
+ dispatch_or_exit(s, scope: 'build') || exit(NOTHING_TO_DO_EXIT)
849
+ end
850
+
851
+ # Dispatch, or print the server's complaint and exit 1. nil means there's
852
+ # nothing to do (said on STDERR so `request_work` STDOUT stays pure JSON).
853
+ def dispatch_or_exit(s, scope:, sha: nil, capabilities: capabilities_for(s))
854
+ rec = s.dispatch(scope: scope, sha: sha, capabilities: capabilities)
855
+ if rec.nil?
856
+ $stderr.puts 'MesaTestHub has nothing more for this computer to ' \
857
+ "#{scope == 'build' ? 'build' : 'test on this commit'} right now."
858
+ elsif scope == 'build'
859
+ $stderr.puts "MesaTestHub suggests #{rec['short_sha']} on #{rec['branch']}: " \
860
+ "#{Array(rec['reasons']).join('; ')}"
861
+ end
862
+ rec
863
+ rescue TestHubError => e
864
+ $stderr.puts e.message
865
+ exit 1
866
+ end
867
+
369
868
  def emit_search_response(response)
370
869
  if response.is_a?(Net::HTTPSuccess)
371
870
  puts response.body
@@ -374,6 +873,10 @@ class MesaTest < Thor
374
873
  $stderr.puts "Search request failed: HTTP #{response.code} "\
375
874
  "#{response.message}"
376
875
  $stderr.puts response.body unless response.body.nil? || response.body.empty?
876
+ if response.code == '401'
877
+ $stderr.puts "\nCheck your API key (`mesa_test setup`, or MESATESTHUB_API_KEY).\n" \
878
+ "#{MesaTestSubmitter::API_KEY_HELP}"
879
+ end
377
880
  exit 1
378
881
  end
379
882
  end
@@ -404,11 +907,16 @@ class MesaTest < Thor
404
907
  puts computer_check['message']
405
908
  else
406
909
  $stderr.puts computer_check['message']
910
+ unless computer_check['reachable'] == false
911
+ $stderr.puts 'Check your API key (`mesa_test setup`, or MESATESTHUB_API_KEY).'
912
+ end
407
913
  exit 1
408
914
  end
409
915
  end
410
916
 
411
- def create_submitter(force: true)
917
+ # hub: true for commands that talk to MesaTestHub, which all need an API
918
+ # key; checked first, before any checkout or build.
919
+ def create_submitter(force: true, hub: false)
412
920
  s = case MODE
413
921
  when :development
414
922
  MesaTestSubmitter.new_from_config(base_uri: DEVELOPMENT_URI)
@@ -420,11 +928,12 @@ class MesaTest < Thor
420
928
  unless force
421
929
  s.setup unless s.confirm_computer_data
422
930
  end
931
+ s.require_api_key! if hub
423
932
  s
424
933
  end
425
934
 
426
935
  def create_and_check_submitter(force: true)
427
- s = create_submitter(force: force)
936
+ s = create_submitter(force: force, hub: true)
428
937
  check_user_and_computer s
429
938
  s
430
939
  end
@@ -467,4 +976,13 @@ class MesaTest < Thor
467
976
  end
468
977
 
469
978
  # actually start the CLI
470
- MesaTest.start(ARGV)
979
+ begin
980
+ MesaTest.start(ARGV)
981
+ rescue Interrupt
982
+ # run_workers has already stopped any tests running in parallel.
983
+ $stderr.puts "\nInterrupted."
984
+ exit 130
985
+ rescue TestHubError => e
986
+ $stderr.puts e.message
987
+ exit 1
988
+ end