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.
- checksums.yaml +4 -4
- data/bin/mesa_test +673 -136
- data/lib/mesa_test.rb +734 -295
- metadata +9 -5
data/lib/mesa_test.rb
CHANGED
|
@@ -8,11 +8,33 @@ require 'net/https'
|
|
|
8
8
|
require 'thor'
|
|
9
9
|
require 'json'
|
|
10
10
|
require 'base64'
|
|
11
|
+
require 'digest'
|
|
11
12
|
|
|
12
13
|
MesaDirError = Class.new(StandardError)
|
|
13
14
|
TestCaseDirError = Class.new(StandardError)
|
|
14
15
|
InvalidDataType = Class.new(StandardError)
|
|
15
16
|
GitHubError = Class.new(StandardError)
|
|
17
|
+
TestHubError = Class.new(StandardError)
|
|
18
|
+
|
|
19
|
+
# Output conventions (CLAUDE.md, "Output"): a bold step heading after one
|
|
20
|
+
# blank line for each phase; green when MESA work succeeded (a build, a
|
|
21
|
+
# passing test, a standalone submission), red when something failed, yellow
|
|
22
|
+
# for a warning (the run continues, but you may want to act), blue only for
|
|
23
|
+
# setup-wizard questions; everything else plain. No color for plumbing.
|
|
24
|
+
def say_step(shell, text)
|
|
25
|
+
shell.say ''
|
|
26
|
+
shell.say text, :bold
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def short_sha(sha)
|
|
30
|
+
sha.to_s[0, 7]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A warning on STDERR (so `request_work`'s JSON stays clean), yellow on a
|
|
34
|
+
# terminal.
|
|
35
|
+
def warn_yellow(text)
|
|
36
|
+
$stderr.puts($stderr.tty? ? "\e[33m#{text}\e[0m" : text)
|
|
37
|
+
end
|
|
16
38
|
|
|
17
39
|
GITHUB_HTTPS = 'https://github.com/MESAHub/mesa.git'.freeze
|
|
18
40
|
GITHUB_SSH = 'git@github.com:MESAHub/mesa.git'.freeze
|
|
@@ -20,86 +42,122 @@ GITHUB_SSH = 'git@github.com:MESAHub/mesa.git'.freeze
|
|
|
20
42
|
class MesaTestSubmitter
|
|
21
43
|
DEFAULT_URI = 'https://testhub.mesastar.org'.freeze
|
|
22
44
|
|
|
45
|
+
# How to get an API key, for the setup wizard and for every command that
|
|
46
|
+
# finds none.
|
|
47
|
+
API_KEY_HELP = <<~HELP.freeze
|
|
48
|
+
mesa_test authenticates with a per-computer API key. To get one:
|
|
49
|
+
1. Log in to MESATestHub (#{DEFAULT_URI}) and open My Computers.
|
|
50
|
+
2. Open this computer's page (add the computer first if it isn't
|
|
51
|
+
listed; a computer used only for `search` and `count` is fine).
|
|
52
|
+
3. Click "Generate key" and copy the key, which starts with mth_.
|
|
53
|
+
HELP
|
|
54
|
+
|
|
23
55
|
# set up config file for computer
|
|
24
56
|
def setup
|
|
25
57
|
update do |s|
|
|
26
|
-
shell.say
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
response =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# Get user password
|
|
48
|
-
response = shell.ask 'What is the password associated with the email ' \
|
|
49
|
-
"#{s.email} (required)? (#{s.password})", :blue
|
|
50
|
-
s.password = response unless response.empty?
|
|
58
|
+
shell.say <<~INTRO
|
|
59
|
+
This wizard sets up this computer's profile for MESATestHub. Each
|
|
60
|
+
question shows the current value; press enter to keep it. You can
|
|
61
|
+
review everything at the end.
|
|
62
|
+
|
|
63
|
+
#{API_KEY_HELP}
|
|
64
|
+
(MESATESTHUB_API_KEY, if set, overrides the key saved here.) The key
|
|
65
|
+
can only act as this one computer, can't log in to the website, and
|
|
66
|
+
can be revoked on its own, but it is stored in plain text in
|
|
67
|
+
#{config_file}, as is the logs token.
|
|
68
|
+
INTRO
|
|
69
|
+
|
|
70
|
+
ask_for_api_key(s)
|
|
71
|
+
|
|
72
|
+
# Optional run modes this computer can serve when MESATestHub asks for
|
|
73
|
+
# them (see `mesa_test request_work` and `install_and_test best`)
|
|
74
|
+
current = s.capabilities.select { |_, on| on }.keys.join(', ')
|
|
75
|
+
response = question('Which optional test modes can this computer run when a ' \
|
|
76
|
+
'commit asks for them? Any of: full_inlists, fpe, converge, ' \
|
|
77
|
+
"separated by commas, or 'none'.", current.empty? ? 'none' : current)
|
|
78
|
+
s.capabilities = parse_capabilities(response) unless response.empty?
|
|
51
79
|
|
|
52
80
|
# Get API key for submitting failure logs
|
|
53
|
-
response =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"key)? (#{s.logs_token})", :blue
|
|
81
|
+
response = question('What is your token for uploading failure logs to the logs ' \
|
|
82
|
+
"server? (If you don't have one, ask a MESA testing maintainer " \
|
|
83
|
+
'or a representative from Flatiron for a key.)', s.logs_token)
|
|
57
84
|
s.logs_token = response unless response.empty?
|
|
58
85
|
|
|
59
86
|
# Determine if we'll use ssh or https to access github
|
|
60
|
-
|
|
61
|
-
|
|
87
|
+
shell.say ''
|
|
88
|
+
shell.say 'When accessing GitHub, which protocol do you want to use?', :blue
|
|
89
|
+
response = shell.ask '>', limited_to: %w[ssh https], default: s.github_protocol.to_s
|
|
62
90
|
s.github_protocol = response.strip.downcase.to_sym
|
|
63
91
|
|
|
64
92
|
# Get location of source MESA repo (the mirror)
|
|
65
|
-
response =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"(#{s.mesa_mirror}):", :blue
|
|
93
|
+
response = question('Where is/should your mirrored MESA repository be? This is ' \
|
|
94
|
+
'where a mirror will be stored from which test repos will be ' \
|
|
95
|
+
"generated. You won't touch this in regular operation.", s.mesa_mirror)
|
|
69
96
|
s.mesa_mirror = response unless response.empty?
|
|
70
97
|
|
|
71
98
|
# Get location of source MESA work (where testing happens)
|
|
72
|
-
response =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"(#{s.mesa_work}):", :blue
|
|
99
|
+
response = question('Where is/should your working directory for testing be? This ' \
|
|
100
|
+
'is where testing actually occurs, but all files it uses are ' \
|
|
101
|
+
'cached in the mirror repo to save time later.', s.mesa_work)
|
|
76
102
|
s.mesa_work = response unless response.empty?
|
|
77
|
-
|
|
78
|
-
# Get platform information
|
|
79
|
-
response = shell.ask 'What is the platform of this computer (eg. ' \
|
|
80
|
-
"macOS, Ubuntu)? (#{s.platform}):", :blue
|
|
81
|
-
s.platform = response unless response.empty?
|
|
82
|
-
response = shell.ask 'What is the version of the platform (eg. 10.15.5, ' \
|
|
83
|
-
"Ubuntu 16.04)? (#{s.platform_version}):", :blue
|
|
84
|
-
s.platform_version = response unless response.empty?
|
|
85
|
-
|
|
86
|
-
# we are powerless to do change the location for now, so stop asking
|
|
87
|
-
# about it
|
|
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?
|
|
92
103
|
end
|
|
93
104
|
|
|
94
105
|
# Confirm data. If not confirmed, restart whole wizard.
|
|
95
106
|
if confirm_computer_data
|
|
96
107
|
save_computer_data
|
|
97
108
|
else
|
|
98
|
-
shell.say
|
|
109
|
+
shell.say 'Starting over.'
|
|
99
110
|
setup
|
|
100
111
|
end
|
|
101
112
|
end
|
|
102
113
|
|
|
114
|
+
# Ask one wizard question; returns the stripped answer, '' to keep
|
|
115
|
+
# +current+. The question goes out through `say`, which the terminal
|
|
116
|
+
# wraps normally; the prompt that Readline draws stays a short, plain
|
|
117
|
+
# "> ". Readline counts color codes as visible characters, so a long or
|
|
118
|
+
# colored prompt garbles at the terminal's edge (the start of the line
|
|
119
|
+
# repeats before it wraps).
|
|
120
|
+
def question(text, current = nil)
|
|
121
|
+
shell.say ''
|
|
122
|
+
shell.say(current.to_s.empty? ? text : "#{text} (current: #{current})", :blue)
|
|
123
|
+
shell.ask('>').to_s.strip
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Ask for the API key until MESATestHub accepts one, and take the computer
|
|
127
|
+
# name from it, since a key belongs to exactly one computer. If the hub
|
|
128
|
+
# can't be reached, keep the key and ask for the name instead.
|
|
129
|
+
def ask_for_api_key(s)
|
|
130
|
+
loop do
|
|
131
|
+
if ENV['MESATESTHUB_API_KEY']
|
|
132
|
+
shell.say "Using the API key in MESATESTHUB_API_KEY (#{s.masked_api_key}). " \
|
|
133
|
+
'Unset it to enter one here.', :yellow
|
|
134
|
+
else
|
|
135
|
+
response = question('What is the MESATestHub API key for this computer?',
|
|
136
|
+
s.masked_api_key || 'none')
|
|
137
|
+
s.api_key = response unless response.empty?
|
|
138
|
+
next shell.say('An API key is required.', :red) unless s.api_key
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
check = s.confirm_computer(computer_name: nil)
|
|
142
|
+
if check['valid']
|
|
143
|
+
s.computer_name = check['computer'] || s.computer_name
|
|
144
|
+
shell.say "Key accepted: this computer is #{s.computer_name} on MESATestHub.", :green
|
|
145
|
+
return
|
|
146
|
+
elsif check['reachable'] == false
|
|
147
|
+
shell.say 'Saving the key unverified.', :yellow
|
|
148
|
+
response = question('What is the name of this computer on MESATestHub?', s.computer_name)
|
|
149
|
+
s.computer_name = response unless response.empty?
|
|
150
|
+
return
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
shell.say check['message'], :red
|
|
154
|
+
if ENV['MESATESTHUB_API_KEY']
|
|
155
|
+
raise TestHubError, 'MESATESTHUB_API_KEY was refused; fix or unset it.'
|
|
156
|
+
end
|
|
157
|
+
s.api_key = nil
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
103
161
|
def self.new_from_config(
|
|
104
162
|
config_file: File.join(ENV['HOME'], '.mesa_test', 'config.yml'),
|
|
105
163
|
force_setup: false,
|
|
@@ -107,6 +165,9 @@ e-mail and password will be stored in plain text.'
|
|
|
107
165
|
)
|
|
108
166
|
new_submitter = new(config_file: config_file, base_uri: base_uri)
|
|
109
167
|
if force_setup
|
|
168
|
+
# Start the wizard from the current settings, including a 1.x
|
|
169
|
+
# config's (logs token, directories), so upgrading keeps them.
|
|
170
|
+
new_submitter.load_computer_data if File.exist? config_file
|
|
110
171
|
new_submitter.setup
|
|
111
172
|
elsif not File.exist? config_file
|
|
112
173
|
puts "No such config file #{config_file}. Starting setup wizard."
|
|
@@ -116,45 +177,41 @@ e-mail and password will be stored in plain text.'
|
|
|
116
177
|
return new_submitter
|
|
117
178
|
end
|
|
118
179
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
180
|
+
# Optional run modes a commit can request through its message
|
|
181
|
+
# (`[ci optional]`, `[ci fpe]`, `[ci converge]`), in MESATestHub's names.
|
|
182
|
+
CAPABILITIES = %i[full_inlists fpe converge].freeze
|
|
183
|
+
|
|
184
|
+
attr_accessor :computer_name, :user_name,
|
|
185
|
+
:mesa_mirror, :mesa_work, :processor,
|
|
186
|
+
:config_file, :base_uri, :github_protocol, :logs_token,
|
|
187
|
+
:capabilities
|
|
188
|
+
attr_writer :api_key
|
|
122
189
|
|
|
123
190
|
attr_reader :shell
|
|
124
191
|
|
|
125
192
|
# many defaults are set in body
|
|
126
193
|
def initialize(
|
|
127
|
-
computer_name: nil, user_name: nil,
|
|
128
|
-
mesa_mirror: nil,
|
|
194
|
+
computer_name: nil, user_name: nil, github_protocol: nil,
|
|
195
|
+
mesa_mirror: nil, processor: nil,
|
|
129
196
|
config_file: nil, base_uri: nil, logs_token: nil
|
|
130
197
|
)
|
|
131
198
|
@computer_name = computer_name || Socket.gethostname.scan(/^[^\.]+\.?/)[0]
|
|
132
199
|
@computer_name.chomp!('.') if @computer_name
|
|
133
200
|
@user_name = user_name || (ENV['USER'] || ENV['USERNAME'])
|
|
134
|
-
@email = email || ''
|
|
135
|
-
@password = password || ''
|
|
136
201
|
@github_protocol = github_protocol || :ssh
|
|
137
202
|
@mesa_mirror = mesa_mirror ||
|
|
138
203
|
File.join(ENV['HOME'], '.mesa_test', 'mirror')
|
|
139
204
|
@mesa_work = mesa_work ||
|
|
140
205
|
File.join(ENV['HOME'], '.mesa_test', 'work')
|
|
141
|
-
@platform = platform
|
|
142
|
-
if @platform.nil?
|
|
143
|
-
@platform =
|
|
144
|
-
if OS.osx?
|
|
145
|
-
'macOS'
|
|
146
|
-
elsif OS.linux?
|
|
147
|
-
'Linux'
|
|
148
|
-
else
|
|
149
|
-
''
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
@platform_version = platform_version || ''
|
|
153
206
|
@processor = processor || ''
|
|
154
207
|
@config_file = config_file || File.join(ENV['HOME'], '.mesa_test',
|
|
155
208
|
'config.yml')
|
|
156
|
-
|
|
209
|
+
# MESATESTHUB_URL points the client at another testhub (a local dev
|
|
210
|
+
# server, staging) without editing MODE in bin/mesa_test.
|
|
211
|
+
@base_uri = ENV['MESATESTHUB_URL'] || base_uri
|
|
157
212
|
@logs_token = logs_token || ENV['MESA_LOGS_TOKEN']
|
|
213
|
+
@api_key = nil
|
|
214
|
+
@capabilities = CAPABILITIES.map { |c| [c, false] }.to_h
|
|
158
215
|
|
|
159
216
|
# set up thor-proof way to get responses from user. Thor hijacks the
|
|
160
217
|
# gets command, so we have to use its built-in "ask" method, which is
|
|
@@ -169,71 +226,144 @@ e-mail and password will be stored in plain text.'
|
|
|
169
226
|
yield self if block_given?
|
|
170
227
|
end
|
|
171
228
|
|
|
229
|
+
# The per-computer API key: MESATESTHUB_API_KEY from the environment wins
|
|
230
|
+
# over the config file, so a cluster job can supply it without the key
|
|
231
|
+
# ever being written to disk. nil means no key (see #require_api_key!).
|
|
232
|
+
def api_key
|
|
233
|
+
key = ENV['MESATESTHUB_API_KEY'] || @api_key
|
|
234
|
+
key.nil? || key.strip.empty? ? nil : key.strip
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Every MESATestHub request needs a key, so commands that talk to the hub
|
|
238
|
+
# call this first, before checking anything out or building.
|
|
239
|
+
def require_api_key!
|
|
240
|
+
return if api_key
|
|
241
|
+
|
|
242
|
+
raise TestHubError, <<~MSG
|
|
243
|
+
No MESATestHub API key is set. (This version of mesa_test no longer
|
|
244
|
+
signs in with your email and password.)
|
|
245
|
+
|
|
246
|
+
#{API_KEY_HELP}
|
|
247
|
+
Then run `mesa_test setup` and paste it when asked, or export
|
|
248
|
+
MESATESTHUB_API_KEY, which overrides the config file (e.g. to keep the
|
|
249
|
+
key off disk on a cluster).
|
|
250
|
+
MSG
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def masked_api_key
|
|
254
|
+
key = api_key
|
|
255
|
+
key && "#{key[0, 10]}..."
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# "fpe, full_inlists" => { full_inlists: true, fpe: true, converge: false }
|
|
259
|
+
def parse_capabilities(text)
|
|
260
|
+
wanted = text.downcase.split(/[\s,]+/).map(&:to_sym)
|
|
261
|
+
CAPABILITIES.map { |c| [c, wanted.include?(c)] }.to_h
|
|
262
|
+
end
|
|
263
|
+
|
|
172
264
|
def confirm_computer_data
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
# For one "computer" on the web server, and for [subjective] consistency
|
|
191
|
-
# reasons, the platform, processor, and RAM cannot be changed! If you
|
|
192
|
-
# change platforms (i.e. switch from mac to linux, or change between linux
|
|
193
|
-
# flavors), you should create a new computer account. Similarly, create new
|
|
194
|
-
# computer accounts if you change your RAM or processor. You do not need
|
|
195
|
-
# to change computers if you upgrade your platform (macOS 10.12 -> 10.13
|
|
265
|
+
modes = capabilities.select { |_, on| on }.keys.join(', ')
|
|
266
|
+
say_step(shell, "This computer's settings")
|
|
267
|
+
[['Computer', computer_name],
|
|
268
|
+
['API key', masked_api_key || 'none'],
|
|
269
|
+
['Optional test modes', modes.empty? ? 'none' : modes],
|
|
270
|
+
['Logs token', logs_token.to_s.empty? ? 'none' : logs_token],
|
|
271
|
+
['GitHub protocol', github_protocol],
|
|
272
|
+
['MESA mirror', mesa_mirror],
|
|
273
|
+
['Work directory', mesa_work],
|
|
274
|
+
['OS (detected)', platform_version]].each do |label, value|
|
|
275
|
+
shell.say format(' %-20s %s', label, value)
|
|
276
|
+
end
|
|
277
|
+
shell.say ''
|
|
278
|
+
shell.ask('Save these settings? [y/N]').to_s.strip.casecmp('y').zero?
|
|
279
|
+
end
|
|
280
|
+
|
|
196
281
|
def save_computer_data
|
|
197
282
|
data_hash = {
|
|
198
283
|
'computer_name' => computer_name,
|
|
199
|
-
'
|
|
200
|
-
'
|
|
284
|
+
'api_key' => @api_key,
|
|
285
|
+
'capabilities' => capabilities.select { |_, on| on }.keys.map(&:to_s),
|
|
201
286
|
'logs_token' => logs_token,
|
|
202
287
|
'github_protocol' => github_protocol,
|
|
203
288
|
'mesa_mirror' => mesa_mirror,
|
|
204
|
-
'mesa_work' => mesa_work
|
|
205
|
-
'platform' => platform,
|
|
206
|
-
'platform_version' => platform_version,
|
|
289
|
+
'mesa_work' => mesa_work
|
|
207
290
|
}
|
|
208
291
|
# make sure there's a directory to write to
|
|
209
292
|
unless dir_or_symlink_exists? File.dirname(config_file)
|
|
210
293
|
FileUtils.mkdir_p File.dirname(config_file)
|
|
211
294
|
end
|
|
212
|
-
|
|
295
|
+
# Owner-only: the file holds the API key and logs token.
|
|
296
|
+
File.write(config_file, YAML.dump(data_hash), perm: 0o600)
|
|
297
|
+
File.chmod(0o600, config_file)
|
|
213
298
|
end
|
|
214
299
|
|
|
300
|
+
# A setting missing from the file (e.g. capabilities in a 1.x config)
|
|
301
|
+
# keeps its default.
|
|
215
302
|
def load_computer_data
|
|
216
|
-
data_hash = YAML.load(File.read(config_file))
|
|
217
|
-
@computer_name = data_hash
|
|
218
|
-
@
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
@
|
|
223
|
-
@
|
|
224
|
-
@
|
|
225
|
-
@
|
|
303
|
+
data_hash = YAML.load(File.read(config_file)) || {}
|
|
304
|
+
@computer_name = data_hash.fetch('computer_name', @computer_name)
|
|
305
|
+
@api_key = data_hash.fetch('api_key', @api_key)
|
|
306
|
+
if data_hash.key?('capabilities')
|
|
307
|
+
@capabilities = parse_capabilities(Array(data_hash['capabilities']).join(','))
|
|
308
|
+
end
|
|
309
|
+
@logs_token = data_hash['logs_token'] || @logs_token
|
|
310
|
+
@github_protocol = (data_hash['github_protocol'] || @github_protocol).to_sym
|
|
311
|
+
@mesa_mirror = data_hash.fetch('mesa_mirror', @mesa_mirror)
|
|
312
|
+
@mesa_work = data_hash.fetch('mesa_work', @mesa_work)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# This computer's OS version, detected (once per run) rather than
|
|
316
|
+
# configured, since a typed-in value goes stale with the first OS update.
|
|
317
|
+
# MESATestHub shows it after the computer's platform (macOS / linux), so
|
|
318
|
+
# macOS sends just the number and Linux the distribution:
|
|
319
|
+
# "27.0.1", "Rocky Linux 8.10", "Ubuntu 24.04 (WSL)".
|
|
320
|
+
def platform_version
|
|
321
|
+
@platform_version ||= self.class.detect_platform_version
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def self.detect_platform_version
|
|
325
|
+
version = OS.mac? ? `sw_vers -productVersion 2>/dev/null`.strip : linux_version
|
|
326
|
+
version = `uname -sr 2>/dev/null`.strip if version.to_s.empty?
|
|
327
|
+
version
|
|
328
|
+
rescue StandardError
|
|
329
|
+
`uname -sr 2>/dev/null`.strip rescue ''
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# The freedesktop os-release standard (every current distribution, and
|
|
333
|
+
# containers; /usr/lib/os-release is its fallback location):
|
|
334
|
+
# NAME="Rocky Linux", VERSION_ID="8.10". Rolling releases (Arch) have no
|
|
335
|
+
# VERSION_ID. RHEL/CentOS 6 predate it but have /etc/redhat-release.
|
|
336
|
+
def self.linux_version
|
|
337
|
+
path = %w[/etc/os-release /usr/lib/os-release].find { |f| File.readable?(f) }
|
|
338
|
+
name =
|
|
339
|
+
if path
|
|
340
|
+
os = File.readlines(path, chomp: true)
|
|
341
|
+
.map { |line| line.split('=', 2) }.select { |kv| kv.size == 2 }
|
|
342
|
+
.to_h { |k, v| [k.strip, v.strip.delete('"\'')] }
|
|
343
|
+
[os['NAME'], os['VERSION_ID']].compact.join(' ')
|
|
344
|
+
elsif File.readable?('/etc/redhat-release')
|
|
345
|
+
# "CentOS release 6.10 (Final)"
|
|
346
|
+
File.read('/etc/redhat-release').strip.sub(/ \(.*\)\z/, '')
|
|
347
|
+
end
|
|
348
|
+
return name if name.to_s.empty?
|
|
349
|
+
|
|
350
|
+
wsl = File.readable?('/proc/version') && File.read('/proc/version').match?(/microsoft/i)
|
|
351
|
+
wsl ? "#{name} (WSL)" : name
|
|
226
352
|
end
|
|
227
353
|
|
|
228
354
|
# Parameters to be submitted in JSON format for reporting information about
|
|
229
|
-
# the submitting
|
|
355
|
+
# the submitting computer. The API key identifies it; the name is a
|
|
356
|
+
# cross-check (the hub refuses a key that belongs to another computer).
|
|
230
357
|
def submitter_params
|
|
231
|
-
{
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
358
|
+
{ computer: computer_name, platform_version: platform_version }
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Headers for every MESATestHub request.
|
|
362
|
+
def testhub_headers(json_body: true)
|
|
363
|
+
headers = { 'Accept' => 'application/json' }
|
|
364
|
+
headers['Content-Type'] = 'application/json' if json_body
|
|
365
|
+
headers['Authorization'] = "Bearer #{api_key}" if api_key
|
|
366
|
+
headers
|
|
237
367
|
end
|
|
238
368
|
|
|
239
369
|
# Parameters to be submitted in JSON format for reporting information about
|
|
@@ -261,28 +391,55 @@ e-mail and password will be stored in plain text.'
|
|
|
261
391
|
# test cases and the test results. These will be encoded as an array of
|
|
262
392
|
# JSON objects.
|
|
263
393
|
def instance_params(mesa)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
394
|
+
submittable_tests(mesa).map(&:results_hash)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Tests with results from *this* build: run, readable, and not older than
|
|
398
|
+
# the build itself. A reused work directory (`install --no-checkout`) can
|
|
399
|
+
# still hold testhub.yml files from an earlier build; those would be filed
|
|
400
|
+
# under the wrong commit, so they're skipped (and counted).
|
|
401
|
+
def submittable_tests(mesa)
|
|
402
|
+
built_at = mesa.built_at
|
|
403
|
+
unread = []
|
|
404
|
+
stale = 0
|
|
405
|
+
tests = mesa.test_case_names.flat_map do |mod, names|
|
|
406
|
+
names.filter_map do |test_name|
|
|
407
|
+
test_case = mesa.test_cases[mod][test_name]
|
|
408
|
+
next unless test_case&.ran?
|
|
409
|
+
|
|
410
|
+
if built_at && File.mtime(test_case.testhub_file) < built_at
|
|
411
|
+
stale += 1
|
|
412
|
+
next
|
|
275
413
|
end
|
|
414
|
+
test_case.results_hash
|
|
415
|
+
test_case
|
|
416
|
+
rescue TestCaseDirError, Psych::Exception
|
|
417
|
+
unread << test_case
|
|
418
|
+
nil
|
|
276
419
|
end
|
|
277
420
|
end
|
|
278
|
-
|
|
279
|
-
shell.say "
|
|
280
|
-
:
|
|
281
|
-
has_errors.each do |test_case|
|
|
282
|
-
shell.say "- #{test_case.test_name}", :red
|
|
283
|
-
end
|
|
421
|
+
if stale.positive?
|
|
422
|
+
shell.say "Skipping #{stale} test result#{'s' unless stale == 1} older than this " \
|
|
423
|
+
'build (left over from an earlier one).', :yellow
|
|
284
424
|
end
|
|
285
|
-
|
|
425
|
+
unless unread.empty?
|
|
426
|
+
shell.say "Couldn't read the results of #{unread.map(&:test_name).join(', ')}; " \
|
|
427
|
+
'not submitting them.', :red
|
|
428
|
+
end
|
|
429
|
+
tests
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# The build's recorded run modes in the submissions API's `claim:` block
|
|
433
|
+
# (use_* flags; informational on the server, which reads the modes each
|
|
434
|
+
# test actually ran with from its testhub.yml).
|
|
435
|
+
def run_mode_params(mesa)
|
|
436
|
+
modes = mesa.recorded_run_modes
|
|
437
|
+
{
|
|
438
|
+
use_fpe: modes[:fpe] == true,
|
|
439
|
+
use_full_inlists: modes[:skip_optional] == false,
|
|
440
|
+
use_converge: modes[:resolution_factor].is_a?(String) ||
|
|
441
|
+
modes[:resolution_factor].is_a?(Numeric)
|
|
442
|
+
}
|
|
286
443
|
end
|
|
287
444
|
|
|
288
445
|
# Parameters for reporting a failed compilation to the logs server
|
|
@@ -314,28 +471,39 @@ e-mail and password will be stored in plain text.'
|
|
|
314
471
|
[test_case.results_hash]
|
|
315
472
|
end
|
|
316
473
|
|
|
317
|
-
# Phone home to testhub and confirm that
|
|
318
|
-
#
|
|
319
|
-
# test later.
|
|
320
|
-
|
|
474
|
+
# Phone home to testhub and confirm that the API key is valid (and, given a
|
|
475
|
+
# computer name, that it belongs to that computer). Useful for confirming
|
|
476
|
+
# that submissions will be accepted before wasting time on a test later.
|
|
477
|
+
# 'reachable' is false when the hub couldn't be asked at all.
|
|
478
|
+
def confirm_computer(computer_name: self.computer_name)
|
|
321
479
|
uri = URI.parse(base_uri + '/check_computer.json')
|
|
322
480
|
https = Net::HTTP.new(uri.hostname, uri.port)
|
|
323
481
|
https.use_ssl = base_uri.include? 'https'
|
|
324
482
|
|
|
325
|
-
request = Net::HTTP::Post.new(
|
|
326
|
-
|
|
327
|
-
)
|
|
328
|
-
request.body = {
|
|
329
|
-
email: email,
|
|
330
|
-
password: password,
|
|
331
|
-
computer_name: computer_name
|
|
332
|
-
}.to_json
|
|
483
|
+
request = Net::HTTP::Post.new(uri, testhub_headers)
|
|
484
|
+
request.body = { computer_name: computer_name }.compact.to_json
|
|
333
485
|
response = testhub_request(https, request)
|
|
334
486
|
# if the hub was unreachable, behave as an unverified computer; the
|
|
335
487
|
# network error has already been reported by testhub_request
|
|
336
|
-
|
|
488
|
+
if response.nil?
|
|
489
|
+
return { 'valid' => false, 'reachable' => false,
|
|
490
|
+
'message' => 'Could not verify this computer with MESATestHub.' }
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# Anything but a 200 (a 429 from the rate limiter, a 5xx, a proxy's HTML
|
|
494
|
+
# error page) carries no valid/message keys, so name the status and the
|
|
495
|
+
# server's error instead of failing with a blank line.
|
|
496
|
+
unless response.is_a?(Net::HTTPOK)
|
|
497
|
+
return { 'valid' => false,
|
|
498
|
+
'message' => 'Could not verify this computer with MESATestHub ' \
|
|
499
|
+
"(HTTP #{response.code}: #{error_message(response)})." }
|
|
500
|
+
end
|
|
337
501
|
|
|
338
502
|
JSON.parse(response.body).to_hash
|
|
503
|
+
rescue JSON::ParserError
|
|
504
|
+
{ 'valid' => false,
|
|
505
|
+
'message' => 'Could not verify this computer: MESATestHub sent an ' \
|
|
506
|
+
'unreadable response.' }
|
|
339
507
|
end
|
|
340
508
|
|
|
341
509
|
# submit entire commit's worth of test cases, OR submit compilation status
|
|
@@ -349,10 +517,7 @@ e-mail and password will be stored in plain text.'
|
|
|
349
517
|
https = Net::HTTP.new(uri.hostname, uri.port)
|
|
350
518
|
https.use_ssl = true if base_uri.include? 'https'
|
|
351
519
|
|
|
352
|
-
request = Net::HTTP::Post.new(
|
|
353
|
-
uri,
|
|
354
|
-
initheader = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }
|
|
355
|
-
)
|
|
520
|
+
request = Net::HTTP::Post.new(uri, testhub_headers)
|
|
356
521
|
|
|
357
522
|
# create the request body for submission to the submissions API
|
|
358
523
|
#
|
|
@@ -363,11 +528,20 @@ e-mail and password will be stored in plain text.'
|
|
|
363
528
|
empty ||= !mesa.installed?
|
|
364
529
|
request_data = {submitter: submitter_params,
|
|
365
530
|
commit: commit_params(mesa, empty: empty, entire: !empty)}
|
|
531
|
+
request_data[:claim] = run_mode_params(mesa) if mesa.recorded_run_modes.any?
|
|
532
|
+
# Fingerprints let MESATestHub ignore results it already has (a retried
|
|
533
|
+
# submit, a head node re-sending what compute nodes sent).
|
|
534
|
+
fingerprints = {}
|
|
535
|
+
fingerprints['build'] = mesa.build_fingerprint if mesa.build_fingerprint
|
|
366
536
|
# don't need test instances if it's an empty submission or if compilation
|
|
367
537
|
# failed
|
|
538
|
+
tests = []
|
|
368
539
|
if !empty && request_data[:commit][:compiled]
|
|
369
|
-
|
|
540
|
+
tests = submittable_tests(mesa)
|
|
541
|
+
request_data[:instances] = tests.map(&:results_hash)
|
|
542
|
+
tests.each { |t| fingerprints[t.fingerprint_key] = t.fingerprint }
|
|
370
543
|
end
|
|
544
|
+
request_data[:fingerprints] = fingerprints unless fingerprints.empty?
|
|
371
545
|
request.body = request_data.to_json
|
|
372
546
|
|
|
373
547
|
# actually do the submission
|
|
@@ -377,41 +551,39 @@ e-mail and password will be stored in plain text.'
|
|
|
377
551
|
# network failure; testhub_request already explained why
|
|
378
552
|
false
|
|
379
553
|
elsif !response.is_a? Net::HTTPCreated
|
|
380
|
-
shell.say "
|
|
381
|
-
"
|
|
382
|
-
"#{response.message}).", :red
|
|
554
|
+
shell.say "MESATestHub refused the results for #{short_sha(mesa.sha)} " \
|
|
555
|
+
"(HTTP #{response.code}: #{error_message(response)}).", :red
|
|
383
556
|
false
|
|
384
557
|
else
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
558
|
+
if !mesa.installed?
|
|
559
|
+
shell.say "Reported the failed build of #{short_sha(mesa.sha)} to MESATestHub."
|
|
560
|
+
elsif empty || tests.empty?
|
|
561
|
+
shell.say "Reported the build of #{short_sha(mesa.sha)} to MESATestHub."
|
|
562
|
+
else
|
|
563
|
+
shell.say "Submitted the build and #{tests.size} test result#{'s' unless tests.size == 1} " \
|
|
564
|
+
"for #{short_sha(mesa.sha)}.", :green
|
|
565
|
+
end
|
|
566
|
+
# Upload the build log if compilation failed. A logs-server problem is
|
|
567
|
+
# only a warning: the results are already in.
|
|
388
568
|
unless mesa.installed?
|
|
389
|
-
|
|
569
|
+
submit_build_log(mesa)
|
|
570
|
+
return true
|
|
390
571
|
end
|
|
391
572
|
|
|
392
|
-
#
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
mesa.test_cases.each do |mod, test_case_hash|
|
|
396
|
-
test_case_hash.each do |tc_name, test_case|
|
|
397
|
-
# get at each individual test case, see if it failed, and if it
|
|
398
|
-
# did, submit its log files
|
|
399
|
-
if !test_case.passed? || force_logs
|
|
400
|
-
res &&= submit_test_log(test_case, skip_passing: !force_logs)
|
|
401
|
-
end
|
|
402
|
-
end
|
|
403
|
-
end
|
|
573
|
+
# logs for the failing tests just submitted (or all, with force_logs)
|
|
574
|
+
tests.each do |test_case|
|
|
575
|
+
submit_test_log(test_case, skip_passing: !force_logs) if !test_case.passed? || force_logs
|
|
404
576
|
end
|
|
405
|
-
|
|
406
|
-
# a true return value means that any and all log submission were
|
|
407
|
-
# successful
|
|
408
|
-
res
|
|
577
|
+
true
|
|
409
578
|
end
|
|
410
579
|
end
|
|
411
580
|
|
|
412
581
|
# submit results for a single test case instance. Does *not* report overall
|
|
413
|
-
# compilation status to testhub. Use an empty commit submission for that
|
|
414
|
-
|
|
582
|
+
# compilation status to testhub. Use an empty commit submission for that.
|
|
583
|
+
# +quiet+ leaves the success line to the caller (a test run's progress
|
|
584
|
+
# line says "submitted"). Returns whether the result went in; a log
|
|
585
|
+
# upload problem is only a warning.
|
|
586
|
+
def submit_instance(mesa, test_case, force_logs: false, quiet: false)
|
|
415
587
|
unless mesa.install_attempted?
|
|
416
588
|
raise MesaDirError, 'No testhub.yml file found in installation; '\
|
|
417
589
|
'must attempt to install before subitting.'
|
|
@@ -421,10 +593,7 @@ e-mail and password will be stored in plain text.'
|
|
|
421
593
|
https = Net::HTTP.new(uri.hostname, uri.port)
|
|
422
594
|
https.use_ssl = true if base_uri.include? 'https'
|
|
423
595
|
|
|
424
|
-
request = Net::HTTP::Post.new(
|
|
425
|
-
uri,
|
|
426
|
-
initheader = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }
|
|
427
|
-
)
|
|
596
|
+
request = Net::HTTP::Post.new(uri, testhub_headers)
|
|
428
597
|
|
|
429
598
|
# create the request body for submission to the submissions API
|
|
430
599
|
#
|
|
@@ -432,7 +601,9 @@ e-mail and password will be stored in plain text.'
|
|
|
432
601
|
# entire (... there is only test case)
|
|
433
602
|
request_data = {submitter: submitter_params,
|
|
434
603
|
commit: commit_params(mesa, empty: false, entire: false),
|
|
435
|
-
instances: single_instance_params(test_case)
|
|
604
|
+
instances: single_instance_params(test_case),
|
|
605
|
+
fingerprints: { test_case.fingerprint_key => test_case.fingerprint }}
|
|
606
|
+
request_data[:claim] = run_mode_params(mesa) if mesa.recorded_run_modes.any?
|
|
436
607
|
request.body = request_data.to_json
|
|
437
608
|
|
|
438
609
|
# actually do the submission
|
|
@@ -442,17 +613,13 @@ e-mail and password will be stored in plain text.'
|
|
|
442
613
|
# network failure; testhub_request already explained why
|
|
443
614
|
return false
|
|
444
615
|
elsif !response.is_a? Net::HTTPCreated
|
|
445
|
-
shell.say "
|
|
446
|
-
"#{
|
|
447
|
-
"#{response.message}).", :red
|
|
616
|
+
shell.say "MESATestHub refused #{test_case.mod}/#{test_case.test_name} " \
|
|
617
|
+
"(HTTP #{response.code}: #{error_message(response)}).", :red
|
|
448
618
|
return false
|
|
449
619
|
else
|
|
450
|
-
shell.say "
|
|
451
|
-
"for commit #{mesa.sha}.", :green
|
|
620
|
+
shell.say "Submitted #{test_case.mod}/#{test_case.test_name}.", :green unless quiet
|
|
452
621
|
# submit logs if test failed
|
|
453
|
-
if !test_case.passed? || force_logs
|
|
454
|
-
return submit_test_log(test_case, skip_passing: !force_logs)
|
|
455
|
-
end
|
|
622
|
+
submit_test_log(test_case, skip_passing: !force_logs) if !test_case.passed? || force_logs
|
|
456
623
|
true
|
|
457
624
|
end
|
|
458
625
|
end
|
|
@@ -468,8 +635,7 @@ e-mail and password will be stored in plain text.'
|
|
|
468
635
|
https.read_timeout = 60
|
|
469
636
|
https.request(request)
|
|
470
637
|
rescue StandardError => e
|
|
471
|
-
shell.say "
|
|
472
|
-
"(#{e.class}: #{e.message}).", :red
|
|
638
|
+
shell.say "Could not reach MESATestHub at #{https.address} (#{e.class}: #{e.message}).", :red
|
|
473
639
|
nil
|
|
474
640
|
end
|
|
475
641
|
|
|
@@ -496,9 +662,8 @@ e-mail and password will be stored in plain text.'
|
|
|
496
662
|
begin
|
|
497
663
|
https.request(req)
|
|
498
664
|
rescue StandardError => e
|
|
499
|
-
shell.say "
|
|
500
|
-
|
|
501
|
-
'not affect test results already sent to the test hub.', :yellow
|
|
665
|
+
shell.say "Could not reach the logs server at #{uri.host} (#{e.class}: #{e.message}); " \
|
|
666
|
+
'skipping the log upload. The results themselves are in.', :yellow
|
|
502
667
|
nil
|
|
503
668
|
end
|
|
504
669
|
end
|
|
@@ -508,12 +673,7 @@ e-mail and password will be stored in plain text.'
|
|
|
508
673
|
# intercept and don't send if mesa was properly installed
|
|
509
674
|
return true if mesa.installed?
|
|
510
675
|
|
|
511
|
-
|
|
512
|
-
unless logs_token
|
|
513
|
-
shell.say 'Cannot submit to logs server; need to set mesa_logs_token '\
|
|
514
|
-
'in the mesa_test config file.'
|
|
515
|
-
return false
|
|
516
|
-
end
|
|
676
|
+
return false unless logs_token?
|
|
517
677
|
|
|
518
678
|
# do submission
|
|
519
679
|
res = submit_logs(build_log_params(mesa))
|
|
@@ -523,13 +683,10 @@ e-mail and password will be stored in plain text.'
|
|
|
523
683
|
# network failure; submit_logs already explained why. Don't fail the run.
|
|
524
684
|
false
|
|
525
685
|
elsif !res.is_a? Net::HTTPOK
|
|
526
|
-
shell.say "
|
|
527
|
-
"#{mesa.sha} (server responded #{res.code} #{res.message}).",
|
|
528
|
-
:red
|
|
686
|
+
shell.say "The logs server refused the build log (HTTP #{res.code} #{res.message}).", :yellow
|
|
529
687
|
false
|
|
530
688
|
else
|
|
531
|
-
shell.say
|
|
532
|
-
"#{mesa.sha}.", :green
|
|
689
|
+
shell.say 'Uploaded the build log to the logs server.'
|
|
533
690
|
true
|
|
534
691
|
end
|
|
535
692
|
end
|
|
@@ -542,13 +699,8 @@ e-mail and password will be stored in plain text.'
|
|
|
542
699
|
return true
|
|
543
700
|
end
|
|
544
701
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
shell.say 'Cannot submit to logs server; need to set mesa_logs_token '\
|
|
548
|
-
'in the mesa_test config file..'
|
|
549
|
-
return false
|
|
550
|
-
end
|
|
551
|
-
|
|
702
|
+
return false unless logs_token?
|
|
703
|
+
|
|
552
704
|
# do submission
|
|
553
705
|
res = submit_logs(test_log_params(test_case))
|
|
554
706
|
|
|
@@ -557,21 +709,29 @@ e-mail and password will be stored in plain text.'
|
|
|
557
709
|
# network failure; submit_logs already explained why. Don't fail the run.
|
|
558
710
|
false
|
|
559
711
|
elsif !res.is_a? Net::HTTPOK
|
|
560
|
-
shell.say "
|
|
561
|
-
"
|
|
562
|
-
"#{res.code} #{res.message}).", :red
|
|
712
|
+
shell.say "The logs server refused the logs for #{test_case.test_name} " \
|
|
713
|
+
"(HTTP #{res.code} #{res.message}).", :yellow
|
|
563
714
|
false
|
|
564
715
|
else
|
|
565
|
-
shell.say "
|
|
566
|
-
"#{test_case.test_name} in commit #{test_case.mesa.sha}.",
|
|
567
|
-
:green
|
|
716
|
+
shell.say "Uploaded the logs for #{test_case.test_name} to the logs server."
|
|
568
717
|
true
|
|
569
718
|
end
|
|
570
719
|
end
|
|
571
720
|
|
|
572
|
-
#
|
|
573
|
-
|
|
574
|
-
|
|
721
|
+
# Whether failure logs can be uploaded; says why not once per run.
|
|
722
|
+
def logs_token?
|
|
723
|
+
return true if logs_token && !logs_token.to_s.empty?
|
|
724
|
+
|
|
725
|
+
unless @warned_no_logs_token
|
|
726
|
+
shell.say 'No logs token is set, so failure logs are not being uploaded ' \
|
|
727
|
+
'(add one with `mesa_test setup`).', :yellow
|
|
728
|
+
@warned_no_logs_token = true
|
|
729
|
+
end
|
|
730
|
+
false
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# GET a search query from the testhub. The query rides in the URL's
|
|
734
|
+
# query_text parameter; the API key in the Authorization header.
|
|
575
735
|
# Returns the raw Net::HTTP response.
|
|
576
736
|
def search(query_text)
|
|
577
737
|
get_search('/test_instances/search.json', query_text)
|
|
@@ -583,23 +743,112 @@ e-mail and password will be stored in plain text.'
|
|
|
583
743
|
get_search('/test_instances/search_count.json', query_text)
|
|
584
744
|
end
|
|
585
745
|
|
|
746
|
+
# What a claimed run is running on, for MESATestHub to show on pending
|
|
747
|
+
# tests (so a long-running claim can be explained: an old machine on two
|
|
748
|
+
# threads, a different SDK). Only what's actually known is sent:
|
|
749
|
+
# - with a built +mesa+, its testhub.yml compiler record (SDK, compiler,
|
|
750
|
+
# math backend), which is accurate even if the shell's SDK changed
|
|
751
|
+
# - otherwise (a build claim, made before compiling) just the SDK the
|
|
752
|
+
# shell has active, from mesasdk_init.sh's MESASDK_VERSION
|
|
753
|
+
# - OMP_NUM_THREADS and this computer's platform version
|
|
754
|
+
def claim_environment(mesa = nil)
|
|
755
|
+
env = {}
|
|
756
|
+
if mesa&.install_attempted?
|
|
757
|
+
env.merge!(mesa.compiler_hash.reject { |_k, v| v.to_s.strip.empty? || v == 'Unknown' })
|
|
758
|
+
end
|
|
759
|
+
sdk = ENV['MESASDK_VERSION'].to_s.strip
|
|
760
|
+
env[:sdk_version] ||= sdk unless sdk.empty?
|
|
761
|
+
threads = ENV['OMP_NUM_THREADS'].to_i
|
|
762
|
+
env[:omp_num_threads] = threads if threads.positive?
|
|
763
|
+
env[:platform_version] = platform_version unless platform_version.to_s.strip.empty?
|
|
764
|
+
env
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# Ask MESATestHub what this computer should do next (POST /api/v1/dispatch).
|
|
768
|
+
# +scope+ is 'build' (which commit to build) or 'test' (which test to run;
|
|
769
|
+
# pass the +sha+ you built). +capabilities+ defaults to the configured
|
|
770
|
+
# optional modes; for a test, +fpe+ should say whether *this build* has
|
|
771
|
+
# FPE checks, since MESA_FPE_CHECKS_ON is a compile-time switch.
|
|
772
|
+
#
|
|
773
|
+
# Returns the recommendation as a Hash (commit_sha, branch, scope,
|
|
774
|
+
# test_case_module, test_case_name, flags, reasons, ...), or nil when
|
|
775
|
+
# there's nothing useful to do. Raises TestHubError on any other answer.
|
|
776
|
+
def dispatch(scope:, sha: nil, capabilities: self.capabilities)
|
|
777
|
+
body = { scope: scope }
|
|
778
|
+
body[:commit_sha] = sha if sha
|
|
779
|
+
CAPABILITIES.each { |c| body[:"can_#{c}"] = !!capabilities[c] }
|
|
780
|
+
response = post_testhub('/api/v1/dispatch', submitter: submitter_params, dispatch: body)
|
|
781
|
+
raise TestHubError, 'Dispatch failed: could not reach MESATestHub.' if response.nil?
|
|
782
|
+
return nil if response.is_a?(Net::HTTPNoContent)
|
|
783
|
+
return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
|
|
784
|
+
|
|
785
|
+
raise TestHubError, "Dispatch failed (HTTP #{response.code}): #{error_message(response)}"
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# Tell MESATestHub this computer is about to build +sha+ (scope 'build') or
|
|
789
|
+
# run a test on it (scope 'test' with +mod+ and +test_case+, or
|
|
790
|
+
# +all_test_cases: true+ for the whole suite). The commit then shows as
|
|
791
|
+
# pending instead of untested. The later submission fulfills the claim by
|
|
792
|
+
# matching computer, commit, and test, so nothing needs remembering here.
|
|
793
|
+
#
|
|
794
|
+
# Claims are advisory: a failure is reported and swallowed (returns nil) so
|
|
795
|
+
# it never stops a test run.
|
|
796
|
+
#
|
|
797
|
+
# The claim also says what the run is running on (see #claim_environment),
|
|
798
|
+
# which MESATestHub shows on the pending test. Pass +mesa+ for test claims
|
|
799
|
+
# so the build's own compiler record is used.
|
|
800
|
+
def claim(sha:, scope:, mod: nil, test_case: nil, all_test_cases: false,
|
|
801
|
+
flags: {}, dispatched_at: nil, mesa: nil)
|
|
802
|
+
body = { commit_sha: sha, scope: scope }
|
|
803
|
+
body.merge!(claim_environment(scope.to_s == 'test' ? mesa : nil))
|
|
804
|
+
body[:test_case_module] = mod.to_s if mod
|
|
805
|
+
body[:test_case_name] = test_case if test_case
|
|
806
|
+
body[:all_test_cases] = true if all_test_cases
|
|
807
|
+
body[:dispatched_at] = dispatched_at if dispatched_at
|
|
808
|
+
CAPABILITIES.each { |c| body[:"use_#{c}"] = !!flags[c] }
|
|
809
|
+
response = post_testhub('/api/v1/claims', submitter: submitter_params, claim: body)
|
|
810
|
+
return nil if response.nil? # unreachable; testhub_request said so
|
|
811
|
+
return JSON.parse(response.body) if response.is_a?(Net::HTTPCreated)
|
|
812
|
+
|
|
813
|
+
shell.say "Could not claim #{claim_label(scope, mod, test_case, all_test_cases)} " \
|
|
814
|
+
"(HTTP #{response.code}: #{error_message(response)}); continuing anyway.",
|
|
815
|
+
:yellow
|
|
816
|
+
nil
|
|
817
|
+
end
|
|
818
|
+
|
|
586
819
|
private
|
|
587
820
|
|
|
821
|
+
# POST JSON to the hub with testhub_request's bounded timeouts. Returns
|
|
822
|
+
# the response, or nil after a network failure (already reported).
|
|
823
|
+
def post_testhub(path, payload)
|
|
824
|
+
uri = URI.parse(base_uri + path)
|
|
825
|
+
https = Net::HTTP.new(uri.hostname, uri.port)
|
|
826
|
+
https.use_ssl = base_uri.include? 'https'
|
|
827
|
+
request = Net::HTTP::Post.new(uri, testhub_headers)
|
|
828
|
+
request.body = payload.to_json
|
|
829
|
+
testhub_request(https, request)
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
def error_message(response)
|
|
833
|
+
JSON.parse(response.body.to_s)['error'] || response.message
|
|
834
|
+
rescue JSON::ParserError
|
|
835
|
+
response.message
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
def claim_label(scope, mod, test_case, all_test_cases)
|
|
839
|
+
return 'the build' if scope.to_s == 'build'
|
|
840
|
+
return 'every test' if all_test_cases
|
|
841
|
+
"#{mod}/#{test_case}"
|
|
842
|
+
end
|
|
843
|
+
|
|
588
844
|
def get_search(path, query_text)
|
|
589
845
|
uri = URI.parse(base_uri + path)
|
|
590
|
-
uri.query = URI.encode_www_form(
|
|
591
|
-
email: email,
|
|
592
|
-
password: password,
|
|
593
|
-
query_text: query_text
|
|
594
|
-
)
|
|
846
|
+
uri.query = URI.encode_www_form(query_text: query_text)
|
|
595
847
|
|
|
596
848
|
https = Net::HTTP.new(uri.hostname, uri.port)
|
|
597
849
|
https.use_ssl = base_uri.include? 'https'
|
|
598
850
|
|
|
599
|
-
request = Net::HTTP::Get.new(
|
|
600
|
-
uri,
|
|
601
|
-
initheader = { 'Accept' => 'application/json' }
|
|
602
|
-
)
|
|
851
|
+
request = Net::HTTP::Get.new(uri, testhub_headers(json_body: false))
|
|
603
852
|
|
|
604
853
|
https.request(request)
|
|
605
854
|
end
|
|
@@ -643,35 +892,29 @@ class Mesa
|
|
|
643
892
|
end
|
|
644
893
|
|
|
645
894
|
def checkout(new_sha: 'HEAD')
|
|
895
|
+
# A branch name is worth showing; a SHA is shown with its subject below.
|
|
896
|
+
say_step(shell, new_sha =~ /\A\h{40}\z/ ? 'Checking out MESA' : "Checking out #{new_sha}")
|
|
646
897
|
# before anything confirm that git-lfs has been installed
|
|
647
|
-
shell.say "\nEnsuring that git-lfs is installed... ", :blue
|
|
648
898
|
command = 'git lfs help >> /dev/null 2>&1'
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
shell.say "no", :red
|
|
653
|
-
raise(GitHubError, "The command #{command} returned with an error "\
|
|
654
|
-
'status, indicating that git-lfs is not installed. '\
|
|
655
|
-
'Make sure it is installed and try again.')
|
|
899
|
+
unless bash_execute(command)
|
|
900
|
+
raise(GitHubError, 'git-lfs is not installed (`git lfs help` failed); ' \
|
|
901
|
+
'MESA needs it. Install it and try again.')
|
|
656
902
|
end
|
|
657
903
|
|
|
658
904
|
# set up mirror if it doesn't exist
|
|
659
905
|
unless dir_or_symlink_exists?(mirror_dir)
|
|
660
|
-
shell.say "
|
|
661
|
-
'
|
|
906
|
+
shell.say "Creating a mirror of MESA in #{mirror_dir} (first time only; " \
|
|
907
|
+
'this can take a while).'
|
|
662
908
|
FileUtils.mkdir_p mirror_dir
|
|
663
909
|
case github_protocol
|
|
664
910
|
when :ssh
|
|
665
911
|
command = "git clone --mirror #{GITHUB_SSH} #{mirror_dir}"
|
|
666
|
-
shell.say command
|
|
667
912
|
# fail loudly if this doesn't work
|
|
668
913
|
unless bash_execute(command)
|
|
669
914
|
# nuke the mirror directory since it is probably bogus (make this
|
|
670
915
|
# code fire off the next time checkout is done)
|
|
671
|
-
shell.say
|
|
672
|
-
|
|
673
|
-
shell.say command
|
|
674
|
-
bash_execute(command)
|
|
916
|
+
shell.say 'Cloning failed; removing the partial mirror.', :red
|
|
917
|
+
bash_execute("rm -rf #{mirror_dir}")
|
|
675
918
|
|
|
676
919
|
raise(GitHubError, 'Error while executing the following command:'\
|
|
677
920
|
"#{command}. Perhaps you haven't set up "\
|
|
@@ -679,15 +922,12 @@ class Mesa
|
|
|
679
922
|
end
|
|
680
923
|
when :https
|
|
681
924
|
command = "git clone --mirror #{GITHUB_HTTPS} #{mirror_dir}"
|
|
682
|
-
shell.say command
|
|
683
925
|
# fail loudly if this doesn't work
|
|
684
926
|
unless bash_execute(command)
|
|
685
927
|
# nuke the mirror directory since it is probably bogus (make this
|
|
686
928
|
# code fire off the next time checkout is done)
|
|
687
|
-
shell.say
|
|
688
|
-
|
|
689
|
-
shell.say command
|
|
690
|
-
bash_execute(command)
|
|
929
|
+
shell.say 'Cloning failed; removing the partial mirror.', :red
|
|
930
|
+
bash_execute("rm -rf #{mirror_dir}")
|
|
691
931
|
|
|
692
932
|
raise(GitHubError, 'Error while executing the following command: '\
|
|
693
933
|
"#{command}. Perhaps you need to configure "\
|
|
@@ -705,20 +945,19 @@ class Mesa
|
|
|
705
945
|
update_mirror
|
|
706
946
|
|
|
707
947
|
# create "work" directory with proper commit
|
|
708
|
-
shell.say "\nSetting up worktree repo...", :blue
|
|
709
948
|
FileUtils.mkdir_p mesa_dir
|
|
710
|
-
command = "git -C #{mirror_dir} worktree add #{mesa_dir} #{new_sha}"
|
|
711
|
-
|
|
712
|
-
|
|
949
|
+
command = "git -C #{mirror_dir} worktree add --quiet #{mesa_dir} #{new_sha}"
|
|
950
|
+
if bash_execute(command)
|
|
951
|
+
shell.say "#{bashticks("git -C #{mesa_dir} log -1 --format='%h %s'")} (in #{mesa_dir})"
|
|
952
|
+
return
|
|
953
|
+
end
|
|
713
954
|
|
|
714
955
|
raise(GitHubError, 'Failed while executing the following command: '\
|
|
715
956
|
"\"#{command}\".")
|
|
716
957
|
end
|
|
717
958
|
|
|
718
959
|
def update_mirror
|
|
719
|
-
|
|
720
|
-
command = "git -C #{mirror_dir} fetch origin --prune"
|
|
721
|
-
shell.say command
|
|
960
|
+
command = "git -C #{mirror_dir} fetch origin --prune --quiet"
|
|
722
961
|
# fail loudly
|
|
723
962
|
return if bash_execute(command)
|
|
724
963
|
|
|
@@ -728,15 +967,11 @@ class Mesa
|
|
|
728
967
|
|
|
729
968
|
def remove
|
|
730
969
|
return unless File.exist? mesa_dir
|
|
731
|
-
shell.say "\nRemoving work directory from worktree (clearing old data)...",
|
|
732
|
-
:blue
|
|
733
970
|
command = "git -C #{mirror_dir} worktree remove --force #{mesa_dir}"
|
|
734
|
-
shell.say command
|
|
735
971
|
return if bash_execute(command)
|
|
736
972
|
|
|
737
|
-
|
|
973
|
+
# Not a worktree of this mirror (or git refused): just delete it.
|
|
738
974
|
command = "rm -rf #{mesa_dir}"
|
|
739
|
-
shell.say command
|
|
740
975
|
# fail loudly (the "true" tells bash_execute to raise an exception if
|
|
741
976
|
# the command fails)
|
|
742
977
|
bash_execute(command, true)
|
|
@@ -754,8 +989,6 @@ class Mesa
|
|
|
754
989
|
with_mesa_dir do
|
|
755
990
|
visit_and_check mesa_dir, MesaDirError, 'E\countered a problem in ' \
|
|
756
991
|
"running `clean` in #{mesa_dir}." do
|
|
757
|
-
shell.say('MESA_DIR = ' + ENV['MESA_DIR'])
|
|
758
|
-
shell.say './clean'
|
|
759
992
|
bash_execute('./clean')
|
|
760
993
|
end
|
|
761
994
|
end
|
|
@@ -766,8 +999,6 @@ class Mesa
|
|
|
766
999
|
with_mesa_dir do
|
|
767
1000
|
visit_and_check mesa_dir, MesaDirError, 'Encountered a problem in ' \
|
|
768
1001
|
"running `install` in #{mesa_dir}." do
|
|
769
|
-
shell.say('MESA_DIR = ' + ENV['MESA_DIR'])
|
|
770
|
-
shell.say './install'
|
|
771
1002
|
bash_execute('./install')
|
|
772
1003
|
end
|
|
773
1004
|
end
|
|
@@ -792,23 +1023,65 @@ class Mesa
|
|
|
792
1023
|
b64_file(build_log)
|
|
793
1024
|
end
|
|
794
1025
|
|
|
1026
|
+
COMPILER_KEYS = %i[compiler compiler_version sdk_version math_backend].freeze
|
|
1027
|
+
|
|
795
1028
|
# sourced from $MESA_DIR/testhub.yml, which should be created after
|
|
796
|
-
# installation
|
|
1029
|
+
# installation. Only the compiler keys are taken: the file also carries
|
|
1030
|
+
# mesa_test's own run-mode block (see #record_run_modes), which doesn't
|
|
1031
|
+
# belong in the commit payload.
|
|
797
1032
|
def compiler_hash
|
|
798
|
-
data_file = File.join(mesa_dir, 'testhub.yml')
|
|
799
1033
|
res = {
|
|
800
1034
|
compiler: 'Unknown',
|
|
801
1035
|
sdk_version: 'Unknown',
|
|
802
1036
|
math_backend: 'Unknown'
|
|
803
1037
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1038
|
+
# testhub.yml has string keys; symbolize them so they replace the
|
|
1039
|
+
# defaults above rather than sitting beside them as duplicate keys
|
|
1040
|
+
# (which json >= 3 refuses to serialize).
|
|
1041
|
+
found = install_data.transform_keys(&:to_sym).slice(*COMPILER_KEYS)
|
|
1042
|
+
res.merge(found)
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
# The key under which `mesa_test install` records, in $MESA_DIR/testhub.yml,
|
|
1046
|
+
# the run modes this build was made for. The file is written by MESA's own
|
|
1047
|
+
# install (math/test), so it is recreated on every build and can't go stale.
|
|
1048
|
+
RUN_MODES_KEY = 'mesa_test_run_modes'.freeze
|
|
1049
|
+
|
|
1050
|
+
# Run modes recorded at install, as a hash with any of
|
|
1051
|
+
# fpe: true/false (compiled with MESA_FPE_CHECKS_ON?)
|
|
1052
|
+
# skip_optional: true/false (MESA_SKIP_OPTIONAL set or unset)
|
|
1053
|
+
# resolution_factor: a factor string, or false for "unset"
|
|
1054
|
+
# Only modes that were explicitly chosen at install are present; anything
|
|
1055
|
+
# absent falls back to the caller's environment.
|
|
1056
|
+
def recorded_run_modes
|
|
1057
|
+
(install_data[RUN_MODES_KEY] || {}).transform_keys(&:to_sym)
|
|
1058
|
+
.slice(*RUN_MODE_KEYS)
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
# Append the modes chosen for this build to testhub.yml. Call after the
|
|
1062
|
+
# install, which rewrites the file.
|
|
1063
|
+
def record_run_modes(modes)
|
|
1064
|
+
data_file = File.join(mesa_dir, 'testhub.yml')
|
|
1065
|
+
return unless File.exist?(data_file)
|
|
1066
|
+
|
|
1067
|
+
modes = modes.slice(*RUN_MODE_KEYS)
|
|
1068
|
+
return if modes.empty?
|
|
1069
|
+
|
|
1070
|
+
block = { RUN_MODES_KEY => modes.transform_keys(&:to_s) }.to_yaml.sub(/\A---\n/, '')
|
|
1071
|
+
File.open(data_file, 'a') do |f|
|
|
1072
|
+
f.puts '# Added by mesa_test install: the run modes this build was made for.'
|
|
1073
|
+
f.puts '# `mesa_test test` applies them unless told otherwise.'
|
|
1074
|
+
f.write block
|
|
809
1075
|
end
|
|
810
1076
|
end
|
|
811
1077
|
|
|
1078
|
+
def install_data
|
|
1079
|
+
data_file = File.join(mesa_dir, 'testhub.yml')
|
|
1080
|
+
return {} unless File.exist?(data_file)
|
|
1081
|
+
|
|
1082
|
+
YAML.safe_load(File.read(data_file)) || {}
|
|
1083
|
+
end
|
|
1084
|
+
|
|
812
1085
|
## TEST SUITE METHODS
|
|
813
1086
|
|
|
814
1087
|
def check_mod(mod)
|
|
@@ -839,7 +1112,7 @@ class Mesa
|
|
|
839
1112
|
@names_to_numbers[mod] = {}
|
|
840
1113
|
@test_case_names[mod] = []
|
|
841
1114
|
@test_cases[mod] = {}
|
|
842
|
-
visit_dir(test_suite_dir(mod: mod)
|
|
1115
|
+
visit_dir(test_suite_dir(mod: mod)) do
|
|
843
1116
|
bashticks('./list_tests').split("\n").each do |line|
|
|
844
1117
|
num, tc_name = line.strip.split
|
|
845
1118
|
@names_to_numbers[mod][tc_name.strip] = num.to_i
|
|
@@ -852,7 +1125,7 @@ class Mesa
|
|
|
852
1125
|
mesa: self
|
|
853
1126
|
)
|
|
854
1127
|
rescue TestCaseDirError
|
|
855
|
-
shell.say "
|
|
1128
|
+
shell.say "do1_test_source lists #{tc_name.strip}, but it has no directory; skipping it.", :yellow
|
|
856
1129
|
end
|
|
857
1130
|
end
|
|
858
1131
|
end
|
|
@@ -899,6 +1172,18 @@ class Mesa
|
|
|
899
1172
|
)
|
|
900
1173
|
end
|
|
901
1174
|
|
|
1175
|
+
# Fingerprint of this build (a hash of build.log), so MESATestHub can
|
|
1176
|
+
# recognize a build report it already has. nil before any build.
|
|
1177
|
+
def build_fingerprint
|
|
1178
|
+
file_fingerprint(File.join(mesa_dir, 'build.log'))
|
|
1179
|
+
end
|
|
1180
|
+
|
|
1181
|
+
# When this build finished (build.log's mtime), or nil.
|
|
1182
|
+
def built_at
|
|
1183
|
+
build_log = File.join(mesa_dir, 'build.log')
|
|
1184
|
+
File.exist?(build_log) ? File.mtime(build_log) : nil
|
|
1185
|
+
end
|
|
1186
|
+
|
|
902
1187
|
def install_attempted?
|
|
903
1188
|
File.exist? File.join(mesa_dir, 'testhub.yml')
|
|
904
1189
|
end
|
|
@@ -909,15 +1194,13 @@ class Mesa
|
|
|
909
1194
|
# change MESA_DIR, holding on to old value
|
|
910
1195
|
orig_mesa_dir = ENV['MESA_DIR']
|
|
911
1196
|
ENV['MESA_DIR'] = mesa_dir
|
|
912
|
-
|
|
913
|
-
|
|
1197
|
+
|
|
914
1198
|
# do the stuff
|
|
915
1199
|
begin
|
|
916
1200
|
yield
|
|
917
1201
|
# make sure we undo MESA_DIR change
|
|
918
1202
|
ensure
|
|
919
1203
|
ENV['MESA_DIR'] = orig_mesa_dir
|
|
920
|
-
shell.say "Changed MESA_DIR back to #{ENV['MESA_DIR']}.", :blue
|
|
921
1204
|
end
|
|
922
1205
|
end
|
|
923
1206
|
|
|
@@ -1073,10 +1356,26 @@ class MesaTestCase
|
|
|
1073
1356
|
File.join(test_case_dir, 'testhub.yml')
|
|
1074
1357
|
end
|
|
1075
1358
|
|
|
1359
|
+
# Run this test like #do_one, but as its own subprocess with its own
|
|
1360
|
+
# working directory and environment (+env+ overrides; nil unsets), so
|
|
1361
|
+
# several can run at once -- #do_one changes the process-wide directory
|
|
1362
|
+
# and expects the environment to be set globally. Output goes to
|
|
1363
|
+
# each_test_run.log in the test's directory. Returns true if
|
|
1364
|
+
# each_test_run exited cleanly (the test's pass/fail is in testhub.yml).
|
|
1365
|
+
def run_isolated(env: {})
|
|
1366
|
+
log = File.join(test_case_dir, 'each_test_run.log')
|
|
1367
|
+
pid = Process.spawn(env, './each_test_run', position.to_s,
|
|
1368
|
+
chdir: test_suite_dir, out: log, err: %i[child out], pgroup: true)
|
|
1369
|
+
RUNNING_TESTS_LOCK.synchronize { RUNNING_TESTS[pid] = test_name }
|
|
1370
|
+
_, status = Process.wait2(pid)
|
|
1371
|
+
status.success?
|
|
1372
|
+
ensure
|
|
1373
|
+
RUNNING_TESTS_LOCK.synchronize { RUNNING_TESTS.delete(pid) } if pid
|
|
1374
|
+
end
|
|
1375
|
+
|
|
1076
1376
|
# just punt to +each_test_run+ in the test_suite directory. It's your problem
|
|
1077
1377
|
# now, sucker!
|
|
1078
1378
|
def do_one
|
|
1079
|
-
shell.say("Testing #{test_name}", :yellow)
|
|
1080
1379
|
visit_dir(test_suite_dir) do
|
|
1081
1380
|
bash_execute("./each_test_run #{position}")
|
|
1082
1381
|
end
|
|
@@ -1090,6 +1389,23 @@ class MesaTestCase
|
|
|
1090
1389
|
YAML.load(File.read(testhub_file))
|
|
1091
1390
|
end
|
|
1092
1391
|
|
|
1392
|
+
# How MESATestHub keys this test's fingerprint: "<module>/<test>".
|
|
1393
|
+
def fingerprint_key
|
|
1394
|
+
"#{mod}/#{test_name}"
|
|
1395
|
+
end
|
|
1396
|
+
|
|
1397
|
+
# Fingerprint of this run's results (a hash of testhub.yml).
|
|
1398
|
+
def fingerprint
|
|
1399
|
+
file_fingerprint(testhub_file)
|
|
1400
|
+
end
|
|
1401
|
+
|
|
1402
|
+
# Whether this test wrote results at or after +time+, i.e. during the
|
|
1403
|
+
# run that started then (not a testhub.yml left from an earlier run).
|
|
1404
|
+
# mtimes can be whole seconds, so compare at that resolution.
|
|
1405
|
+
def ran_since?(time)
|
|
1406
|
+
ran? && File.mtime(testhub_file) >= time.floor
|
|
1407
|
+
end
|
|
1408
|
+
|
|
1093
1409
|
# rough proxy for whether or not the test has even been run
|
|
1094
1410
|
def ran?
|
|
1095
1411
|
File.exist?(testhub_file)
|
|
@@ -1157,12 +1473,8 @@ end
|
|
|
1157
1473
|
# exception if the block failed (returned false or nil)
|
|
1158
1474
|
def visit_and_check(new_dir, exception, message)
|
|
1159
1475
|
cwd = Dir.getwd
|
|
1160
|
-
shell.say "Leaving #{cwd}", :blue
|
|
1161
|
-
shell.say "\nEntering #{new_dir}.", :blue
|
|
1162
1476
|
Dir.chdir(new_dir)
|
|
1163
1477
|
success = yield if block_given?
|
|
1164
|
-
shell.say "Leaving #{new_dir}", :blue
|
|
1165
|
-
shell.say "\nEntering #{cwd}.", :blue
|
|
1166
1478
|
Dir.chdir(cwd)
|
|
1167
1479
|
return if success
|
|
1168
1480
|
raise exception, message
|
|
@@ -1170,19 +1482,15 @@ end
|
|
|
1170
1482
|
|
|
1171
1483
|
# cd into a new directory, execute a block, then cd back into original
|
|
1172
1484
|
# directory
|
|
1173
|
-
def visit_dir(new_dir
|
|
1485
|
+
def visit_dir(new_dir)
|
|
1174
1486
|
cwd = Dir.getwd
|
|
1175
|
-
shell.say "Leaving #{cwd}\n", :blue unless quiet
|
|
1176
|
-
shell.say "\nEntering #{new_dir}.", :blue unless quiet
|
|
1177
1487
|
Dir.chdir(new_dir)
|
|
1178
1488
|
yield if block_given?
|
|
1179
|
-
shell.say "Leaving #{new_dir}\n", :blue unless quiet
|
|
1180
|
-
shell.say "\nRe-entering #{cwd}.", :blue unless quiet
|
|
1181
1489
|
Dir.chdir(cwd)
|
|
1182
1490
|
end
|
|
1183
1491
|
|
|
1184
1492
|
# the next function probalby doesn't belong here, but keep it anyway, please
|
|
1185
|
-
# create seed data for test cases for
|
|
1493
|
+
# create seed data for test cases for MESATestHub of a given mesa version
|
|
1186
1494
|
def generate_seeds_rb(mesa_dir, outfile)
|
|
1187
1495
|
m = Mesa.new(mesa_dir: mesa_dir)
|
|
1188
1496
|
m.load_test_source_data
|
|
@@ -1224,6 +1532,137 @@ def bashticks(command)
|
|
|
1224
1532
|
`bash -c "#{command}"`.chomp
|
|
1225
1533
|
end
|
|
1226
1534
|
|
|
1535
|
+
# Run modes: how MESA is told to run a test, and the environment variables
|
|
1536
|
+
# behind them. MESA's each_test_run records each one in the test's
|
|
1537
|
+
# testhub.yml (fpe_checks / run_optional / resolution_factor), which is what
|
|
1538
|
+
# MESATestHub reads.
|
|
1539
|
+
#
|
|
1540
|
+
# fpe: MESA_FPE_CHECKS_ON=1. Compile-time as well as run-time
|
|
1541
|
+
# (MESA's make reads it), so it belongs to the build.
|
|
1542
|
+
# skip_optional: MESA_SKIP_OPTIONAL set. MESA runs every inlist unless
|
|
1543
|
+
# this is set.
|
|
1544
|
+
# resolution_factor: MESA_TEST_SUITE_RESOLUTION_FACTOR, which is what
|
|
1545
|
+
# `[ci converge]` sets.
|
|
1546
|
+
#
|
|
1547
|
+
# A mode that is nil/absent leaves the variable as the user's shell has it.
|
|
1548
|
+
RUN_MODE_KEYS = %i[fpe skip_optional resolution_factor].freeze
|
|
1549
|
+
DEFAULT_CONVERGE_FACTOR = '0.8'.freeze
|
|
1550
|
+
|
|
1551
|
+
# ENV overrides for +modes+ (nil value = unset the variable).
|
|
1552
|
+
def run_mode_env(modes)
|
|
1553
|
+
env = {}
|
|
1554
|
+
env['MESA_FPE_CHECKS_ON'] = modes[:fpe] ? '1' : nil unless modes[:fpe].nil?
|
|
1555
|
+
unless modes[:skip_optional].nil?
|
|
1556
|
+
env['MESA_SKIP_OPTIONAL'] = modes[:skip_optional] ? 't' : nil
|
|
1557
|
+
end
|
|
1558
|
+
unless modes[:resolution_factor].nil?
|
|
1559
|
+
env['MESA_TEST_SUITE_RESOLUTION_FACTOR'] =
|
|
1560
|
+
modes[:resolution_factor] ? modes[:resolution_factor].to_s : nil
|
|
1561
|
+
end
|
|
1562
|
+
env
|
|
1563
|
+
end
|
|
1564
|
+
|
|
1565
|
+
# The resolution factor a converge run should use: the user's own, if their
|
|
1566
|
+
# shell sets one, else DEFAULT_CONVERGE_FACTOR.
|
|
1567
|
+
def converge_factor
|
|
1568
|
+
ENV['MESA_TEST_SUITE_RESOLUTION_FACTOR'] || DEFAULT_CONVERGE_FACTOR
|
|
1569
|
+
end
|
|
1570
|
+
|
|
1571
|
+
# Run modes a MESATestHub dispatch asked for. Requests only ever *add* to the
|
|
1572
|
+
# user's setup: a mode the hub didn't ask for is left out (so the shell
|
|
1573
|
+
# decides), never forced off.
|
|
1574
|
+
#
|
|
1575
|
+
# A converge request may name its factor (`[ci converge 0.5]` arrives as
|
|
1576
|
+
# rec['converge_factor']). That's the commit author's request, so it wins
|
|
1577
|
+
# over +preferred+ -- a factor from --resolution-factor or the shell's
|
|
1578
|
+
# MESA_TEST_SUITE_RESOLUTION_FACTOR -- with a warning when they differ.
|
|
1579
|
+
# Without a named factor, +preferred+ (else the default) is used.
|
|
1580
|
+
def dispatch_run_modes(rec, preferred: nil, preferred_source: nil)
|
|
1581
|
+
flags = rec['flags'] || {}
|
|
1582
|
+
modes = {}
|
|
1583
|
+
modes[:fpe] = true if flags['use_fpe']
|
|
1584
|
+
modes[:skip_optional] = false if flags['use_full_inlists']
|
|
1585
|
+
if flags['use_converge']
|
|
1586
|
+
requested = rec['converge_factor']
|
|
1587
|
+
preferred ||= ENV['MESA_TEST_SUITE_RESOLUTION_FACTOR']
|
|
1588
|
+
preferred_source ||= 'MESA_TEST_SUITE_RESOLUTION_FACTOR'
|
|
1589
|
+
if requested
|
|
1590
|
+
factor = requested.to_s
|
|
1591
|
+
if preferred && preferred.to_f != requested.to_f
|
|
1592
|
+
warn_yellow "MESATestHub asked for resolution factor #{factor} ([ci converge #{factor}]); " \
|
|
1593
|
+
"using it instead of your #{preferred_source}=#{preferred}."
|
|
1594
|
+
end
|
|
1595
|
+
modes[:resolution_factor] = factor
|
|
1596
|
+
else
|
|
1597
|
+
modes[:resolution_factor] = preferred || DEFAULT_CONVERGE_FACTOR
|
|
1598
|
+
end
|
|
1599
|
+
end
|
|
1600
|
+
modes
|
|
1601
|
+
end
|
|
1602
|
+
|
|
1603
|
+
# Process groups of tests started by MesaTestCase#run_isolated, so an
|
|
1604
|
+
# interrupted parallel run can stop them all.
|
|
1605
|
+
RUNNING_TESTS = {}
|
|
1606
|
+
RUNNING_TESTS_LOCK = Mutex.new
|
|
1607
|
+
|
|
1608
|
+
def stop_running_tests
|
|
1609
|
+
pids = RUNNING_TESTS_LOCK.synchronize { RUNNING_TESTS.keys }
|
|
1610
|
+
pids.each do |pid|
|
|
1611
|
+
Process.kill('TERM', -pid)
|
|
1612
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
1613
|
+
nil
|
|
1614
|
+
end
|
|
1615
|
+
end
|
|
1616
|
+
|
|
1617
|
+
# Run +jobs+ worker threads, each calling the block with its index until
|
|
1618
|
+
# the block returns :done. On Ctrl-C, kills the workers first (so none
|
|
1619
|
+
# goes on to submit a half-run test), then every running test, then
|
|
1620
|
+
# re-raises. Exceptions in a worker propagate when it's joined.
|
|
1621
|
+
def run_workers(jobs)
|
|
1622
|
+
workers = []
|
|
1623
|
+
jobs.times do |i|
|
|
1624
|
+
workers << Thread.new do
|
|
1625
|
+
Thread.current.report_on_exception = false
|
|
1626
|
+
loop { break if yield(i) == :done }
|
|
1627
|
+
end
|
|
1628
|
+
end
|
|
1629
|
+
workers.each(&:join)
|
|
1630
|
+
rescue Interrupt
|
|
1631
|
+
workers.each(&:kill)
|
|
1632
|
+
stop_running_tests
|
|
1633
|
+
raise
|
|
1634
|
+
end
|
|
1635
|
+
|
|
1636
|
+
# Hand each of +items+ to the block, +jobs+ at a time.
|
|
1637
|
+
def parallel_each(items, jobs)
|
|
1638
|
+
queue = Queue.new
|
|
1639
|
+
items.each { |item| queue << item }
|
|
1640
|
+
run_workers([jobs, items.size].min) do
|
|
1641
|
+
item = begin
|
|
1642
|
+
queue.pop(true)
|
|
1643
|
+
rescue ThreadError
|
|
1644
|
+
next :done
|
|
1645
|
+
end
|
|
1646
|
+
yield item
|
|
1647
|
+
nil
|
|
1648
|
+
end
|
|
1649
|
+
end
|
|
1650
|
+
|
|
1651
|
+
# Run the block with +overrides+ applied to ENV (nil deletes a variable),
|
|
1652
|
+
# restoring the previous values afterwards.
|
|
1653
|
+
def with_env(overrides)
|
|
1654
|
+
saved = overrides.keys.map { |k| [k, ENV[k]] }.to_h
|
|
1655
|
+
overrides.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
|
|
1656
|
+
yield
|
|
1657
|
+
ensure
|
|
1658
|
+
saved&.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
|
|
1659
|
+
end
|
|
1660
|
+
|
|
1661
|
+
# SHA-256 of a file's contents, or nil if it doesn't exist.
|
|
1662
|
+
def file_fingerprint(path)
|
|
1663
|
+
File.exist?(path) ? Digest::SHA256.file(path).hexdigest : nil
|
|
1664
|
+
end
|
|
1665
|
+
|
|
1227
1666
|
# encode the contents of a file as base-64
|
|
1228
1667
|
def b64_file(filename)
|
|
1229
1668
|
Base64.encode64(File.open(filename).read)
|