lemans 1.3.5 → 1.5.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/CHANGELOG.md +21 -0
- data/README.md +17 -2
- data/exe/lemans-remote +141 -3
- data/exe/lemans-viewer +962 -0
- data/lib/lemans/agent.rb +4 -1
- data/lib/lemans/agents/miniswen.rb +10 -5
- data/lib/lemans/agents/miniswen_installed.rb +23 -5
- data/lib/lemans/agents/nop.rb +1 -1
- data/lib/lemans/agents/oracle.rb +1 -1
- data/lib/lemans/agents.rb +4 -2
- data/lib/lemans/cli/templates/bench/bench.yml +7 -0
- data/lib/lemans/cli.rb +85 -31
- data/lib/lemans/config/verifier.rb +10 -2
- data/lib/lemans/environments/docker/proxy/Dockerfile +4 -0
- data/lib/lemans/environments/docker/proxy/README.md +49 -0
- data/lib/lemans/environments/docker/proxy/server.rb +79 -0
- data/lib/lemans/environments/docker.rb +79 -16
- data/lib/lemans/result.rb +52 -1
- data/lib/lemans/runner/task.rb +9 -5
- data/lib/lemans/runner.rb +45 -2
- data/lib/lemans/store.rb +6 -0
- data/lib/lemans/stores/fs.rb +16 -4
- data/lib/lemans/trial/patch.rb +21 -0
- data/lib/lemans/trial/setup.rb +1 -0
- data/lib/lemans/trial.rb +109 -41
- data/lib/lemans/version.rb +1 -1
- data/lib/lemans.rb +2 -0
- data/lib/miniswen/agent.rb +65 -18
- data/lib/miniswen/cli.rb +20 -4
- data/lib/miniswen/environment/docker.rb +1 -1
- data/lib/miniswen/jail/proxy.rb +62 -0
- data/lib/miniswen/jail.rb +36 -9
- data/lib/miniswen/local.rb +6 -7
- data/lib/miniswen/testing.rb +7 -1
- data/lib/miniswen/version.rb +1 -1
- metadata +49 -1
|
@@ -12,6 +12,7 @@ module Lemans
|
|
|
12
12
|
HOUSEKEEPING_TIMEOUT = 60
|
|
13
13
|
MAX_OUTPUT_BYTES = 200_000
|
|
14
14
|
EXEC_SLACK = 30
|
|
15
|
+
PROXY_PORT = 3128
|
|
15
16
|
|
|
16
17
|
attr_reader :container
|
|
17
18
|
|
|
@@ -24,7 +25,8 @@ module Lemans
|
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def start
|
|
27
|
-
build_image! if image.built?
|
|
28
|
+
build_image!(image) if image.built?
|
|
29
|
+
start_proxy!(network.hosts) if network.allowlist?
|
|
28
30
|
docker!("run", *run_args, timeout: build_timeout)
|
|
29
31
|
@container = @name
|
|
30
32
|
self
|
|
@@ -37,9 +39,10 @@ module Lemans
|
|
|
37
39
|
timeout ||= DEFAULT_TIMEOUT
|
|
38
40
|
started = now
|
|
39
41
|
argv = [ "exec" ]
|
|
42
|
+
env = proxy_env.merge(env) if network.allowlist?
|
|
40
43
|
env.each { |key, value| argv += [ "--env", "#{key}=#{value}" ] }
|
|
41
44
|
# The in-container timeout is what actually kills the process
|
|
42
|
-
argv += [ container, "timeout", timeout.to_i.to_s, "
|
|
45
|
+
argv += [ container, "timeout", timeout.to_i.to_s, "bash", "-c", command ]
|
|
43
46
|
|
|
44
47
|
exit_code, output = capture("docker", *argv, timeout: timeout + EXEC_SLACK)
|
|
45
48
|
ExecResult.new(command:, exit_code:, output:, duration: (now - started).round(3))
|
|
@@ -59,13 +62,13 @@ module Lemans
|
|
|
59
62
|
def switch_network_policy!(policy)
|
|
60
63
|
assert_policy_supported!(policy)
|
|
61
64
|
|
|
62
|
-
if policy.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
wanted = ("bridge" if policy.public?) || (internal_network if policy.allowlist?)
|
|
66
|
+
networks = connected_networks
|
|
67
|
+
(networks - [ wanted ]).each { docker!("network", "disconnect", it, container) }
|
|
68
|
+
|
|
69
|
+
remove_proxy
|
|
70
|
+
start_proxy!(policy.hosts) if policy.allowlist?
|
|
71
|
+
docker!("network", "connect", wanted, container) if wanted && !networks.include?(wanted)
|
|
69
72
|
|
|
70
73
|
@network = policy
|
|
71
74
|
end
|
|
@@ -81,41 +84,101 @@ module Lemans
|
|
|
81
84
|
end
|
|
82
85
|
rescue StandardError => e
|
|
83
86
|
warn "lemans: container #{container} may still be running — remove failed: #{e.class}: #{e.message}"
|
|
87
|
+
ensure
|
|
88
|
+
remove_proxy
|
|
89
|
+
remove_internal_network
|
|
84
90
|
end
|
|
85
91
|
|
|
86
92
|
private
|
|
87
93
|
|
|
88
94
|
def assert_policy_supported!(policy)
|
|
89
|
-
return
|
|
95
|
+
return unless policy.allowlist? && policy.ip_targets.any?
|
|
90
96
|
|
|
91
|
-
raise ConfigError, "docker:
|
|
97
|
+
raise ConfigError, "docker: an allowlist takes host names only (#{policy.ip_targets.join(", ")})"
|
|
92
98
|
end
|
|
93
99
|
|
|
94
100
|
# The tag is the content digest, so an existing image is the identical thing
|
|
95
|
-
def build_image!
|
|
96
|
-
exists, = capture("docker", "image", "inspect",
|
|
101
|
+
def build_image!(spec)
|
|
102
|
+
exists, = capture("docker", "image", "inspect", spec.name, timeout: HOUSEKEEPING_TIMEOUT)
|
|
97
103
|
return if exists.zero?
|
|
98
104
|
|
|
99
|
-
docker!("build", "--tag",
|
|
105
|
+
docker!("build", "--tag", spec.name, spec.context_dir.to_s, timeout: build_timeout, on_output: @logger)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The task container sits on an internal network with no route out; the
|
|
109
|
+
# proxy is on it and on bridge, so the hosts it allows are the only way out.
|
|
110
|
+
def start_proxy!(hosts)
|
|
111
|
+
image = Config::ImageSpec.dockerfile(Pathname(__dir__).join("docker/proxy/Dockerfile"), slug: "proxy")
|
|
112
|
+
build_image!(image)
|
|
113
|
+
create_internal_network!
|
|
114
|
+
docker!("run", "--detach", "--name", proxy_name, "--network", internal_network,
|
|
115
|
+
*label_args, image.name, *hosts)
|
|
116
|
+
docker!("network", "connect", "bridge", proxy_name)
|
|
117
|
+
wait_for_proxy!
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def wait_for_proxy!(attempts: 50)
|
|
121
|
+
attempts.times do
|
|
122
|
+
_, output = capture("docker", "logs", proxy_name, timeout: HOUSEKEEPING_TIMEOUT)
|
|
123
|
+
return if output.include?("listening on")
|
|
124
|
+
|
|
125
|
+
sleep 0.2
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
raise InfrastructureError, "docker: the allowlist proxy did not start"
|
|
100
129
|
end
|
|
101
130
|
|
|
131
|
+
def proxy_env
|
|
132
|
+
url = "http://#{proxy_name}:#{PROXY_PORT}"
|
|
133
|
+
no_proxy = "localhost,127.0.0.1"
|
|
134
|
+
{ "http_proxy" => url, "https_proxy" => url, "HTTP_PROXY" => url, "HTTPS_PROXY" => url,
|
|
135
|
+
"no_proxy" => no_proxy, "NO_PROXY" => no_proxy }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def create_internal_network!
|
|
139
|
+
exists, = capture("docker", "network", "inspect", internal_network, timeout: HOUSEKEEPING_TIMEOUT)
|
|
140
|
+
return if exists.zero?
|
|
141
|
+
|
|
142
|
+
docker!("network", "create", "--internal", *label_args, internal_network)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def remove_proxy
|
|
146
|
+
capture("docker", "rm", "--force", proxy_name, timeout: HOUSEKEEPING_TIMEOUT)
|
|
147
|
+
rescue StandardError
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def remove_internal_network
|
|
152
|
+
capture("docker", "network", "rm", internal_network, timeout: HOUSEKEEPING_TIMEOUT)
|
|
153
|
+
rescue StandardError
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def proxy_name = "#{@name}-proxy"
|
|
158
|
+
|
|
159
|
+
def internal_network = "#{@name}-net"
|
|
160
|
+
|
|
102
161
|
def run_args
|
|
103
162
|
args = [ "--detach", "--init", "--name", @name,
|
|
104
163
|
"--cpus", resources.cpus.to_s, "--memory", "#{resources.memory}m",
|
|
105
164
|
"--cap-add", "SYS_ADMIN", "--cap-add", "NET_ADMIN", "--security-opt", "apparmor=unconfined",
|
|
106
165
|
"--entrypoint", "sh" ]
|
|
107
166
|
args += [ "--network", "none" ] if network.none?
|
|
167
|
+
args += [ "--network", internal_network ] if network.allowlist?
|
|
108
168
|
env.each { |key, value| args += [ "--env", "#{key}=#{value}" ] }
|
|
109
|
-
|
|
110
|
-
args + [ image.name, "-c", "tail -f /dev/null" ]
|
|
169
|
+
args + label_args + [ image.name, "-c", "tail -f /dev/null" ]
|
|
111
170
|
end
|
|
112
171
|
|
|
172
|
+
def label_args = labels.flat_map { |key, value| [ "--label", "#{key}=#{value}" ] }
|
|
173
|
+
|
|
113
174
|
def connected_networks
|
|
114
175
|
docker!("inspect", "--format", "{{range $name, $_ := .NetworkSettings.Networks}}{{$name}} {{end}}", container).split
|
|
115
176
|
end
|
|
116
177
|
|
|
117
178
|
def remove
|
|
118
179
|
capture("docker", "rm", "--force", "--volumes", @name, timeout: HOUSEKEEPING_TIMEOUT)
|
|
180
|
+
remove_proxy
|
|
181
|
+
remove_internal_network
|
|
119
182
|
rescue StandardError
|
|
120
183
|
nil
|
|
121
184
|
end
|
data/lib/lemans/result.rb
CHANGED
|
@@ -136,6 +136,12 @@ module Lemans
|
|
|
136
136
|
def as_json(**) = to_h
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
+
Restart = Data.define(:trial, :step, :mode) do
|
|
140
|
+
def initialize(trial:, step:, mode: nil) = super
|
|
141
|
+
|
|
142
|
+
def as_json(**) = to_h.compact
|
|
143
|
+
end
|
|
144
|
+
|
|
139
145
|
Step = Data.define(:outcome, :usage, :duration) do
|
|
140
146
|
def as_json(**) = { outcome: outcome.as_json, usage: usage&.as_json, duration: }.compact
|
|
141
147
|
|
|
@@ -150,7 +156,7 @@ module Lemans
|
|
|
150
156
|
attr_reader :id, :task, :agent, :model, :index,
|
|
151
157
|
:profile_digest, :task_digest, :revision
|
|
152
158
|
|
|
153
|
-
attr_accessor :tags, :metadata
|
|
159
|
+
attr_accessor :tags, :metadata, :restarted_from
|
|
154
160
|
|
|
155
161
|
attr_reader :phases, :steps
|
|
156
162
|
|
|
@@ -171,6 +177,7 @@ module Lemans
|
|
|
171
177
|
@metadata = {}
|
|
172
178
|
@phases = []
|
|
173
179
|
@steps = nil
|
|
180
|
+
@restarted_from = nil
|
|
174
181
|
|
|
175
182
|
@id = id || "#{task}__#{SecureRandom.alphanumeric(7)}"
|
|
176
183
|
@outcome = Outcome.new(:pending)
|
|
@@ -239,12 +246,55 @@ module Lemans
|
|
|
239
246
|
self
|
|
240
247
|
end
|
|
241
248
|
|
|
249
|
+
# A step is settled once the next one started: it passed its gate and left a savepoint.
|
|
250
|
+
def settled_steps
|
|
251
|
+
names = phases.map(&:name)
|
|
252
|
+
(1..steps.to_a.size).count { names.include?(:"agent.#{it + 1}") }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# The step whose verification ran last, nil when none did
|
|
256
|
+
def verified_step
|
|
257
|
+
name = phases.reverse.find { it.name.to_s.match?(/\Averifier(\.\d+)?\z/) }&.name or return nil
|
|
258
|
+
name.to_s[/\.(\d+)\z/, 1]&.to_i || steps&.size || 1
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Carries over the steps before `step`; a reverification carries `step` itself too.
|
|
262
|
+
def restart!(source, step:, mode: nil)
|
|
263
|
+
@restarted_from = Restart.new(trial: source.id, step:, mode: mode&.to_s)
|
|
264
|
+
carried = mode == :reverify ? step : step - 1
|
|
265
|
+
|
|
266
|
+
if source.steps
|
|
267
|
+
source.steps.first(carried).each { step_completed!(it.outcome, it.usage, duration: it.duration) }
|
|
268
|
+
elsif carried.positive?
|
|
269
|
+
# A single-step run graded before keeps its agent outcome; a failed grading lost it.
|
|
270
|
+
completed!(source.scored? ? source.outcome : Outcome.new(:completed), source.usage)
|
|
271
|
+
end
|
|
272
|
+
self
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Splices in the named phases of the source run, shifted so that its
|
|
276
|
+
# `through` phase ends now, and moves this run's environment setup back in
|
|
277
|
+
# front of them: the timeline reads as one run.
|
|
278
|
+
def adopt_phases!(source, names, through: names.last, now: Time.now.utc)
|
|
279
|
+
delta = now - source.phases.find { it.name == through }.finished_at
|
|
280
|
+
|
|
281
|
+
setup = phases.first
|
|
282
|
+
shift = source.phases.find { it.name == setup.name }.finished_at + delta - setup.finished_at
|
|
283
|
+
phases[0] = Phase.new(setup.name, started_at: setup.started_at + shift, finished_at: setup.finished_at + shift)
|
|
284
|
+
|
|
285
|
+
source.phases.select { names.include?(it.name) }.each do |phase|
|
|
286
|
+
phases << Phase.new(phase.name, started_at: phase.started_at + delta, finished_at: phase.finished_at + delta)
|
|
287
|
+
end
|
|
288
|
+
self
|
|
289
|
+
end
|
|
290
|
+
|
|
242
291
|
private def aggregate_usage = steps.filter_map(&:usage).reduce(:+)
|
|
243
292
|
|
|
244
293
|
def as_json(**)
|
|
245
294
|
{
|
|
246
295
|
trial: id, task:, agent:, model:, index:,
|
|
247
296
|
profile_digest:, task_digest:, revision: revision&.as_json,
|
|
297
|
+
restarted_from: restarted_from&.as_json,
|
|
248
298
|
lemans_version: VERSION,
|
|
249
299
|
tags:, metadata:, phases: phases.map(&:as_json),
|
|
250
300
|
steps: steps&.map(&:as_json),
|
|
@@ -264,6 +314,7 @@ module Lemans
|
|
|
264
314
|
)
|
|
265
315
|
result.tags = data[:tags] || []
|
|
266
316
|
result.metadata = data[:metadata] || {}
|
|
317
|
+
result.restarted_from = Restart.new(**data[:restarted_from]) if data[:restarted_from]
|
|
267
318
|
phases_from(data).each { result.phases << it }
|
|
268
319
|
|
|
269
320
|
# Steps first: the stored outcome/usage below override the aggregates.
|
data/lib/lemans/runner/task.rb
CHANGED
|
@@ -20,18 +20,22 @@ module Lemans
|
|
|
20
20
|
def_delegators :definition, :name, :config
|
|
21
21
|
def_delegators :result, :id
|
|
22
22
|
|
|
23
|
-
private attr_reader :definition, :store, :reporter
|
|
23
|
+
private attr_reader :definition, :store, :reporter, :restart_from, :restart_mode
|
|
24
24
|
|
|
25
|
-
def initialize(model, task_definition, index: 0, store: nil, reporter: nil)
|
|
25
|
+
def initialize(model, task_definition, index: 0, store: nil, reporter: nil, restart_from: nil, restart_mode: nil)
|
|
26
26
|
@model = model
|
|
27
27
|
@definition = task_definition
|
|
28
28
|
@index = index
|
|
29
29
|
@store = store
|
|
30
30
|
@reporter = reporter
|
|
31
|
+
@restart_from = restart_from
|
|
32
|
+
@restart_mode = restart_mode
|
|
31
33
|
@status = :pending
|
|
32
34
|
|
|
33
|
-
# prepare the result object: it's used by the actual execution down the stack
|
|
34
|
-
|
|
35
|
+
# prepare the result object: it's used by the actual execution down the stack;
|
|
36
|
+
# a restart keeps the agent of the run it restarts
|
|
37
|
+
@result = Result.from_task(definition, index:, model: model || config.models.first,
|
|
38
|
+
**({ agent: restart_from.agent } if restart_from))
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
def with_reporter(reporter)
|
|
@@ -55,7 +59,7 @@ module Lemans
|
|
|
55
59
|
private
|
|
56
60
|
|
|
57
61
|
def execute!
|
|
58
|
-
Trial.new(definition, model, store:, result:).run
|
|
62
|
+
Trial.new(definition, model, store:, result:, agent: restart_from&.agent, restart_from:, restart_mode:).run
|
|
59
63
|
end
|
|
60
64
|
end
|
|
61
65
|
end
|
data/lib/lemans/runner.rb
CHANGED
|
@@ -20,20 +20,26 @@ module Lemans
|
|
|
20
20
|
|
|
21
21
|
attr_reader :config, :tasks, :store, :reporter
|
|
22
22
|
|
|
23
|
-
private attr_reader :resuming, :executor
|
|
23
|
+
private attr_reader :resuming, :restarts, :restart_mode, :allow_scored, :executor
|
|
24
24
|
|
|
25
|
-
def initialize(config, tasks, store: nil, reporter: nil, executor: nil, resume: false
|
|
25
|
+
def initialize(config, tasks, store: nil, reporter: nil, executor: nil, resume: false,
|
|
26
|
+
restarts: [], restart_mode: nil, allow_scored: false)
|
|
26
27
|
@config = config
|
|
27
28
|
@tasks = tasks
|
|
28
29
|
@store = store
|
|
29
30
|
@reporter = reporter
|
|
30
31
|
@executor = executor || Executor.new(config.concurrency)
|
|
31
32
|
@resuming = resume
|
|
33
|
+
@restarts = restarts
|
|
34
|
+
@restart_mode = restart_mode
|
|
35
|
+
@allow_scored = allow_scored
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
def resuming? = @resuming
|
|
35
39
|
|
|
36
40
|
def attempts
|
|
41
|
+
return @attempts ||= restart_attempts if restarts.any?
|
|
42
|
+
|
|
37
43
|
@attempts ||= config.agent.models.flat_map do |model|
|
|
38
44
|
@tasks.flat_map do |task|
|
|
39
45
|
completed = resuming? ? completed_attempts(task, model) : 0
|
|
@@ -64,6 +70,43 @@ module Lemans
|
|
|
64
70
|
|
|
65
71
|
private
|
|
66
72
|
|
|
73
|
+
# Every run is checked before any starts: one refused run stops the batch.
|
|
74
|
+
def restart_attempts
|
|
75
|
+
refusals = []
|
|
76
|
+
attempts = restarts.filter_map do |restart|
|
|
77
|
+
restart_attempt(restart)
|
|
78
|
+
rescue ConfigError => e
|
|
79
|
+
refusals << e.message
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
raise ConfigError, refusals.join("\n") if refusals.any?
|
|
83
|
+
|
|
84
|
+
attempts
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# A run named for a restart is restarted on purpose: a changed task or
|
|
88
|
+
# bench is the operator's call, and a reverification may grade a scored run.
|
|
89
|
+
def restart_attempt(restart)
|
|
90
|
+
refuse = ->(reason) { raise ConfigError, "cannot restart #{restart.id}: #{reason}" }
|
|
91
|
+
task = tasks.find { it.name == restart.task }
|
|
92
|
+
refuse.("task #{restart.task} is not in the bench") unless task
|
|
93
|
+
refuse.("it is already scored (--allow-scored to restart it anyway)") if restart.scored? && restart_mode != :reverify && !allow_scored
|
|
94
|
+
|
|
95
|
+
case restart_mode
|
|
96
|
+
when :reverify
|
|
97
|
+
refuse.("it never reached a verification") unless restart.verified_step
|
|
98
|
+
when :recover
|
|
99
|
+
refuse.("#{restart.agent} cannot recover a session") unless Agents.lookup(restart.agent).recoverable?
|
|
100
|
+
|
|
101
|
+
history = task.multistep? ? "agent.result.#{restart.settled_steps + 1}.json" : "agent.result.json"
|
|
102
|
+
refuse.("it has no #{history} to recover from") unless store&.read_artifact(restart, history)
|
|
103
|
+
else
|
|
104
|
+
refuse.("no step was settled before it failed") if restart.settled_steps.zero?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
Task.new(restart.model, task, store:, index: restart.index || 1, restart_from: restart, restart_mode:)
|
|
108
|
+
end
|
|
109
|
+
|
|
67
110
|
def completed_attempts(task, model)
|
|
68
111
|
completed_runs.count do |run|
|
|
69
112
|
run.task == task.name &&
|
data/lib/lemans/store.rb
CHANGED
|
@@ -46,5 +46,11 @@ module Lemans
|
|
|
46
46
|
def read_artifact(result, path)
|
|
47
47
|
raise NotImplementedError
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
# Returns the sorted paths of the artifacts the result stored (the result
|
|
51
|
+
# record is not one of them)
|
|
52
|
+
def artifacts(result)
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
end
|
|
49
55
|
end
|
|
50
56
|
end
|
data/lib/lemans/stores/fs.rb
CHANGED
|
@@ -52,8 +52,8 @@ module Lemans
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def delete(result)
|
|
55
|
-
dir =
|
|
56
|
-
return unless dir
|
|
55
|
+
dir = result_dir(result)
|
|
56
|
+
return unless dir.directory?
|
|
57
57
|
|
|
58
58
|
FileUtils.remove_entry(dir.to_s)
|
|
59
59
|
prune_empty_parents(dir.parent)
|
|
@@ -90,6 +90,13 @@ module Lemans
|
|
|
90
90
|
file.read if file.file?
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
def artifacts(result)
|
|
94
|
+
dir = result_dir(result)
|
|
95
|
+
return [] unless dir.directory?
|
|
96
|
+
|
|
97
|
+
(dir.glob("**/*").select(&:file?).map { it.relative_path_from(dir).to_s } - [ FILENAME ]).sort
|
|
98
|
+
end
|
|
99
|
+
|
|
93
100
|
private
|
|
94
101
|
|
|
95
102
|
def filtered(text) = filterer ? filterer.filter(text) : text
|
|
@@ -132,9 +139,14 @@ module Lemans
|
|
|
132
139
|
tmp&.delete if tmp&.exist?
|
|
133
140
|
end
|
|
134
141
|
|
|
135
|
-
# result.json is stored at <root>/<model-short>/<result-id
|
|
142
|
+
# result.json is stored at <root>/<model-short>/<result-id>; a tree
|
|
143
|
+
# arranged by hand (runs grouped under batch directories) is searched
|
|
144
|
+
# for the id instead.
|
|
136
145
|
def result_dir(result)
|
|
137
|
-
root.join((result.model || result.agent).to_s.split("/").last.tr("#", "-"), result.id)
|
|
146
|
+
canonical = root.join((result.model || result.agent).to_s.split("/").last.to_s.tr("#", "-"), result.id)
|
|
147
|
+
return canonical if canonical.directory?
|
|
148
|
+
|
|
149
|
+
root.glob("**/#{result.id}").find(&:directory?) || canonical
|
|
138
150
|
end
|
|
139
151
|
end
|
|
140
152
|
end
|
data/lib/lemans/trial/patch.rb
CHANGED
|
@@ -61,6 +61,15 @@ module Lemans
|
|
|
61
61
|
raise InfrastructureError, "could not savepoint the tree the step left behind" unless savepoint
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
# Brings a fresh tree to where an earlier run left it: the settled steps'
|
|
65
|
+
# patches applied in order and marked as the savepoint, then the work of
|
|
66
|
+
# the step that run stopped in, on top of it, so collect! sees it as this step's.
|
|
67
|
+
def replay!(settled, unsettled = nil)
|
|
68
|
+
settled.each { apply!(it) }
|
|
69
|
+
savepoint!
|
|
70
|
+
apply!(unsettled) if unsettled
|
|
71
|
+
end
|
|
72
|
+
|
|
64
73
|
# Puts the workdir back to the savepoint exactly: the verifier restored
|
|
65
74
|
# the graded surfaces from the baseline and may have littered the tree,
|
|
66
75
|
# and the next step's agent must find neither.
|
|
@@ -75,6 +84,18 @@ module Lemans
|
|
|
75
84
|
|
|
76
85
|
private
|
|
77
86
|
|
|
87
|
+
def apply!(contents)
|
|
88
|
+
return if contents.empty?
|
|
89
|
+
|
|
90
|
+
Tempfile.create(%w[agent .patch]) do |file|
|
|
91
|
+
file.write(contents)
|
|
92
|
+
file.flush
|
|
93
|
+
environment.upload(file.path, REMOTE_PATCH)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
environment.exec!("#{git} apply --binary --whitespace=nowarn #{REMOTE_PATCH} && rm -f #{REMOTE_PATCH}", timeout:)
|
|
97
|
+
end
|
|
98
|
+
|
|
78
99
|
def save_diff(result, store, from, to, destination)
|
|
79
100
|
diffed = environment.exec("#{git} diff --binary #{from} #{to} > #{REMOTE_PATCH}", timeout:)
|
|
80
101
|
return unless diffed.success?
|
data/lib/lemans/trial/setup.rb
CHANGED
|
@@ -52,6 +52,7 @@ module Lemans
|
|
|
52
52
|
workdir = Shellwords.escape(task.environment.workdir)
|
|
53
53
|
environment.exec!(
|
|
54
54
|
"cd #{workdir} && git apply --binary --whitespace=nowarn #{Shellwords.escape(seed)} && " \
|
|
55
|
+
"find . -path ./.git -prune -o -exec touch -h {} + && " \
|
|
55
56
|
"rm -rf .git && git init -q && git add -A && " \
|
|
56
57
|
"git -c user.name=lemans -c user.email=lemans@localhost commit -qm 'Initial commit'",
|
|
57
58
|
timeout:
|