lean-runtime 2.0.5__tar.gz → 2.0.7__tar.gz
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.
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/CHANGELOG.md +14 -0
- {lean_runtime-2.0.5/lean_runtime.egg-info → lean_runtime-2.0.7}/PKG-INFO +1 -1
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/python-api.md +7 -3
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/ready-programs.md +6 -3
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/engine.py +7 -1
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/result.py +14 -1
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/environments.py +59 -1
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/oci.py +66 -44
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/run_cli.py +10 -5
- {lean_runtime-2.0.5 → lean_runtime-2.0.7/lean_runtime.egg-info}/PKG-INFO +1 -1
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/pyproject.toml +1 -1
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/schemas/execution-v1.schema.json +1 -1
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/discovery/test_engine.py +26 -1
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_bundles.py +52 -2
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_interactive.py +19 -4
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_programs.py +1 -3
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_run_cli.py +71 -5
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_schemas.py +1 -1
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/LICENSE +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/MANIFEST.in +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/README.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/catalog/environments.toml +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/catalog/locks/core-v4.32.2.lock.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/compatibility/README.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/compatibility/mathlib-4.32.2.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/compatibility/mathlib-4.32.2.toml +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/architecture.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/captures.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/case-study-v1.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/cli.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/development.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/environments.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/getting-started.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/index.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/local-projects.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/portable-copies.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/standalone-files.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/trust-and-limitations.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/docs/v1-precision.md +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/examples/mathlib.toml +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/__init__.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/__main__.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/_git.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/_paths.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/backends.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/bundles.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/cli.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/comparison.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/decisions.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/diagnostics.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/__init__.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/analyzer.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/api.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/candidate.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/catalog.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/catalog_build.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/catalog_cli.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/catalog_manifest.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/data/catalog.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/defaults.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/errors.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/module_inventory.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/planner.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/policy.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/probe.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/py.typed +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/schema_resources.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/errors.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/events.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/facade.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/frontmatter.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/health.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/lake.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/lockfiles.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/locking.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/matrix.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/models.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/policies.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/profiling.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/programs.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/projects.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/publisher_verification.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/py.typed +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/references.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/resolver.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/runtime.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/schema_resources.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/serialization.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/specs.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/store.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/timings.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/toolchains.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/verification.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/wire.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime.egg-info/SOURCES.txt +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime.egg-info/dependency_links.txt +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime.egg-info/entry_points.txt +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime.egg-info/requires.txt +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime.egg-info/top_level.txt +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/mkdocs.yml +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/schemas/cleanup-v1.schema.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/schemas/comparison-v1.schema.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/schemas/inspect-v1.schema.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/schemas/matrix-v1.schema.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/schemas/profile-v1.schema.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/schemas/verify-v1.schema.json +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/scripts/run_compatibility.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/scripts/run_v1_case_study.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/scripts/smoke_wheel.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/setup.cfg +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/conftest.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/discovery/e2e/test_real_discovery.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/discovery/test_analyzer.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/discovery/test_catalog.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/discovery/test_catalog_build.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/discovery/test_discovery_schema_resources.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/discovery/test_planner.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/discovery/test_policy.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_backend.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_cli.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_diagnostics.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_environment_integration.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_events_health.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_facade.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_frontmatter.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_git.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_lockfiles.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_multifile.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_paths.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_prebuilt_policy.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_projects.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_references.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_runtime.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_schema_resources.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_signatures.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_specs.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_store.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_toolchains.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_v1_precision.py +0 -0
- {lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/test_verification_inventory.py +0 -0
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 2.0.7
|
|
6
|
+
|
|
7
|
+
- Retry an OCI layer once from a clean partial after size or SHA-256 integrity
|
|
8
|
+
verification fails, bypassing intermediary caches while preserving strict
|
|
9
|
+
digest validation and emitting structured retry diagnostics.
|
|
10
|
+
|
|
11
|
+
## 2.0.6
|
|
12
|
+
|
|
13
|
+
- Preserve the highest-ranked Lean compiler rejection when automatic discovery
|
|
14
|
+
exhausts its candidates, including raw output and parsed diagnostics in both
|
|
15
|
+
human and JSON `lean-run` output.
|
|
16
|
+
- Add line-oriented and NDJSON request/response helpers to `InteractiveSession`
|
|
17
|
+
for incremental REPLs and ready-to-run checker programs.
|
|
18
|
+
|
|
5
19
|
## 2.0.5
|
|
6
20
|
|
|
7
21
|
- Integrate bounded exact-environment discovery into the `lean-runtime` package.
|
|
@@ -179,9 +179,7 @@ with environment.spawn_interactive(
|
|
|
179
179
|
policy=ExecutionPolicy(timeout_seconds=3600, memory_mb=4096),
|
|
180
180
|
) as session:
|
|
181
181
|
request = {"id": 1, "method": "get_info", "params": {}}
|
|
182
|
-
session.
|
|
183
|
-
session.stdin.flush()
|
|
184
|
-
response = json.loads(session.stdout.readline())
|
|
182
|
+
response = session.request_json(request)
|
|
185
183
|
assert session.running
|
|
186
184
|
|
|
187
185
|
result = session.close()
|
|
@@ -199,6 +197,12 @@ the original text is still returned to the caller. Callers of tools that emit
|
|
|
199
197
|
substantial data on both streams should drain both streams to avoid ordinary
|
|
200
198
|
subprocess pipe backpressure.
|
|
201
199
|
|
|
200
|
+
For line-oriented tools, `send_line()` and `read_line()` provide incremental
|
|
201
|
+
REPL-style access. `request_line()` serializes one request/response exchange,
|
|
202
|
+
and `request_json()` does the same for NDJSON protocols. The request helpers
|
|
203
|
+
are locked so concurrent callers cannot interleave protocol frames. The raw
|
|
204
|
+
`stdin`, `stdout`, and `stderr` pipes remain available for other protocols.
|
|
205
|
+
|
|
202
206
|
## Progress events
|
|
203
207
|
|
|
204
208
|
```python
|
|
@@ -20,11 +20,14 @@ program = runtime.create_program(
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
with program.spawn_interactive() as session:
|
|
23
|
-
session.
|
|
24
|
-
session.stdin.flush()
|
|
25
|
-
print(session.stdout.readline())
|
|
23
|
+
print(session.request_line("hello"))
|
|
26
24
|
```
|
|
27
25
|
|
|
26
|
+
For an NDJSON checker, `session.request_json({...})` keeps the compiled process
|
|
27
|
+
alive and returns one decoded response per request. This is the fast path for
|
|
28
|
+
large batches: prepare and verify the checker once, then stream compact inputs
|
|
29
|
+
instead of elaborating a new Lean source file for every item.
|
|
30
|
+
|
|
28
31
|
`create_program` records the program's files and computer compatibility. Opening
|
|
29
32
|
it later with `runtime.program(program.id)` verifies that neither has changed.
|
|
30
33
|
|
|
@@ -336,11 +336,17 @@ def discover(
|
|
|
336
336
|
DISCOVERY_EXHAUSTED,
|
|
337
337
|
f"Lean did not accept the source in any of {len(attempts)} tested candidates",
|
|
338
338
|
)
|
|
339
|
+
rejection_diagnostics = tuple(
|
|
340
|
+
diagnostic
|
|
341
|
+
for attempt in attempts
|
|
342
|
+
if attempt.status == "lean_rejected"
|
|
343
|
+
for diagnostic in attempt.diagnostics
|
|
344
|
+
)
|
|
339
345
|
return DiscoveryResult(
|
|
340
346
|
status="not_found",
|
|
341
347
|
confidence="exhausted",
|
|
342
348
|
plan=plan,
|
|
343
349
|
attempts=tuple(attempts),
|
|
344
350
|
duration_seconds=time.monotonic() - started,
|
|
345
|
-
diagnostics=(reason,),
|
|
351
|
+
diagnostics=(reason, *rejection_diagnostics),
|
|
346
352
|
)
|
|
@@ -124,9 +124,22 @@ class DiscoveryResult:
|
|
|
124
124
|
compiled = next((item for item in self.attempts if item.status == "compiled"), None)
|
|
125
125
|
return compiled.environment_id if compiled is not None else None
|
|
126
126
|
|
|
127
|
+
@property
|
|
128
|
+
def rejection_attempt(self) -> CandidateAttempt | None:
|
|
129
|
+
"""Return the highest-ranked attempt for which Lean rejected the source."""
|
|
130
|
+
return next(
|
|
131
|
+
(
|
|
132
|
+
attempt
|
|
133
|
+
for attempt in self.attempts
|
|
134
|
+
if attempt.status == "lean_rejected" and attempt.execution_result is not None
|
|
135
|
+
),
|
|
136
|
+
None,
|
|
137
|
+
)
|
|
138
|
+
|
|
127
139
|
def raise_for_error(self) -> DiscoveryResult:
|
|
128
140
|
if self.status != "found":
|
|
129
|
-
|
|
141
|
+
details = [item.detail for item in self.diagnostics]
|
|
142
|
+
detail = ": ".join(dict.fromkeys(details)) if details else self.status
|
|
130
143
|
raise DiscoveryError(f"Lean environment discovery did not succeed: {detail}")
|
|
131
144
|
return self
|
|
132
145
|
|
|
@@ -17,7 +17,7 @@ from contextlib import suppress
|
|
|
17
17
|
from dataclasses import asdict, dataclass
|
|
18
18
|
from datetime import datetime, timezone
|
|
19
19
|
from pathlib import Path, PurePosixPath
|
|
20
|
-
from typing import Any, Generic, TextIO, TypeVar, cast
|
|
20
|
+
from typing import Any, Generic, Literal, TextIO, TypeVar, cast
|
|
21
21
|
|
|
22
22
|
from ._git import git_command
|
|
23
23
|
from ._paths import remove_tree
|
|
@@ -291,6 +291,7 @@ class InteractiveSession:
|
|
|
291
291
|
self._cleanup = cleanup
|
|
292
292
|
self._result: ExecutionResult | None = None
|
|
293
293
|
self._closing = threading.Lock()
|
|
294
|
+
self._io = threading.RLock()
|
|
294
295
|
|
|
295
296
|
@property
|
|
296
297
|
def stdin(self) -> TextIO:
|
|
@@ -316,6 +317,63 @@ class InteractiveSession:
|
|
|
316
317
|
"""Return the process exit code, or ``None`` while it is running."""
|
|
317
318
|
return self._process.poll()
|
|
318
319
|
|
|
320
|
+
def _reader(self, stream: Literal["stdout", "stderr"]) -> InteractiveTextReader:
|
|
321
|
+
if stream == "stdout":
|
|
322
|
+
return self.stdout
|
|
323
|
+
if stream == "stderr":
|
|
324
|
+
return self.stderr
|
|
325
|
+
raise ValueError("stream must be 'stdout' or 'stderr'")
|
|
326
|
+
|
|
327
|
+
def send_line(self, line: str) -> None:
|
|
328
|
+
"""Send and flush one line-oriented protocol request."""
|
|
329
|
+
if not isinstance(line, str):
|
|
330
|
+
raise TypeError("interactive request must be a string")
|
|
331
|
+
if "\n" in line or "\r" in line:
|
|
332
|
+
raise ValueError("interactive request must contain exactly one line")
|
|
333
|
+
with self._io:
|
|
334
|
+
if not self.running:
|
|
335
|
+
raise EnvironmentError("interactive process is not running")
|
|
336
|
+
try:
|
|
337
|
+
self.stdin.write(line + "\n")
|
|
338
|
+
self.stdin.flush()
|
|
339
|
+
except (BrokenPipeError, OSError, ValueError) as exc:
|
|
340
|
+
raise EnvironmentError("interactive process did not accept the request") from exc
|
|
341
|
+
|
|
342
|
+
def read_line(self, *, stream: Literal["stdout", "stderr"] = "stdout") -> str:
|
|
343
|
+
"""Read one response line from stdout or stderr."""
|
|
344
|
+
with self._io:
|
|
345
|
+
response = self._reader(stream).readline()
|
|
346
|
+
if response == "":
|
|
347
|
+
raise EnvironmentError(
|
|
348
|
+
f"interactive process ended before producing a {stream} response"
|
|
349
|
+
)
|
|
350
|
+
return response.removesuffix("\n").removesuffix("\r")
|
|
351
|
+
|
|
352
|
+
def request_line(
|
|
353
|
+
self,
|
|
354
|
+
line: str,
|
|
355
|
+
*,
|
|
356
|
+
response_stream: Literal["stdout", "stderr"] = "stdout",
|
|
357
|
+
) -> str:
|
|
358
|
+
"""Atomically send one line and read its corresponding response."""
|
|
359
|
+
with self._io:
|
|
360
|
+
self.send_line(line)
|
|
361
|
+
return self.read_line(stream=response_stream)
|
|
362
|
+
|
|
363
|
+
def request_json(
|
|
364
|
+
self,
|
|
365
|
+
value: object,
|
|
366
|
+
*,
|
|
367
|
+
response_stream: Literal["stdout", "stderr"] = "stdout",
|
|
368
|
+
) -> Any:
|
|
369
|
+
"""Round-trip one newline-delimited JSON value through the live process."""
|
|
370
|
+
request = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
371
|
+
response = self.request_line(request, response_stream=response_stream)
|
|
372
|
+
try:
|
|
373
|
+
return json.loads(response)
|
|
374
|
+
except json.JSONDecodeError as exc:
|
|
375
|
+
raise EnvironmentError("interactive process returned invalid JSON") from exc
|
|
376
|
+
|
|
319
377
|
def close(self) -> ExecutionResult:
|
|
320
378
|
"""Send EOF, stop if necessary, persist provenance, and remove the instance."""
|
|
321
379
|
with self._closing:
|
|
@@ -32,6 +32,7 @@ _BEARER_PARAMETER = re.compile(r'([a-zA-Z]+)="([^"]*)"')
|
|
|
32
32
|
_DIGEST = re.compile(r"sha256:([0-9a-f]{64})")
|
|
33
33
|
_ACCEPT = ", ".join((INDEX_MEDIA_TYPE, MANIFEST_MEDIA_TYPE))
|
|
34
34
|
DEFAULT_ENVIRONMENT_LIBRARIES = ("oci://ghcr.io/alerad/lean-runtime-cache",)
|
|
35
|
+
_BLOB_INTEGRITY_ATTEMPTS = 2
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
class SignatureVerifier(Protocol):
|
|
@@ -215,52 +216,73 @@ class OCIRegistryClient:
|
|
|
215
216
|
return destination
|
|
216
217
|
destination.unlink()
|
|
217
218
|
temporary = destination.with_name(f".{destination.name}.partial")
|
|
218
|
-
|
|
219
|
-
temporary.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
response = self._request(request)
|
|
238
|
-
if offset and response.status != 206:
|
|
239
|
-
offset = 0
|
|
240
|
-
written = 0
|
|
241
|
-
observed = hashlib.sha256()
|
|
219
|
+
for attempt in range(1, _BLOB_INTEGRITY_ATTEMPTS + 1):
|
|
220
|
+
if temporary.exists() and temporary.stat().st_size > size:
|
|
221
|
+
temporary.unlink()
|
|
222
|
+
offset = temporary.stat().st_size if temporary.exists() else 0
|
|
223
|
+
request = urllib.request.Request(self._url(f"blobs/{digest}"))
|
|
224
|
+
if offset:
|
|
225
|
+
request.add_header("Range", f"bytes={offset}-")
|
|
226
|
+
if attempt > 1:
|
|
227
|
+
request.add_header("Cache-Control", "no-cache")
|
|
228
|
+
events.emit(
|
|
229
|
+
"library.layer_download_started",
|
|
230
|
+
"Downloading OCI blob",
|
|
231
|
+
digest=digest,
|
|
232
|
+
size=size,
|
|
233
|
+
attempt=attempt,
|
|
234
|
+
resumed_bytes=offset,
|
|
235
|
+
)
|
|
236
|
+
observed = hashlib.sha256()
|
|
237
|
+
written = offset
|
|
242
238
|
if offset:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
239
|
+
with temporary.open("rb") as existing:
|
|
240
|
+
for chunk in iter(lambda: existing.read(1024 * 1024), b""):
|
|
241
|
+
observed.update(chunk)
|
|
242
|
+
try:
|
|
243
|
+
response = self._request(request)
|
|
244
|
+
if offset and response.status != 206:
|
|
245
|
+
offset = 0
|
|
246
|
+
written = 0
|
|
247
|
+
observed = hashlib.sha256()
|
|
248
|
+
if offset:
|
|
249
|
+
content_range = response.headers.get("Content-Range", "")
|
|
250
|
+
if not content_range.startswith(f"bytes {offset}-"):
|
|
251
|
+
response.close()
|
|
252
|
+
temporary.unlink(missing_ok=True)
|
|
253
|
+
raise EnvironmentError("OCI registry returned an invalid byte range")
|
|
254
|
+
mode = "ab" if offset else "wb"
|
|
255
|
+
with response, temporary.open(mode) as output:
|
|
256
|
+
while chunk := response.read(1024 * 1024):
|
|
257
|
+
written += len(chunk)
|
|
258
|
+
if written > size:
|
|
259
|
+
raise EnvironmentError("OCI blob exceeds its declared size")
|
|
260
|
+
observed.update(chunk)
|
|
261
|
+
output.write(chunk)
|
|
262
|
+
observed_digest = "sha256:" + observed.hexdigest()
|
|
263
|
+
if written == size and observed_digest == digest:
|
|
264
|
+
temporary.replace(destination)
|
|
265
|
+
break
|
|
257
266
|
temporary.unlink(missing_ok=True)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
267
|
+
if attempt == _BLOB_INTEGRITY_ATTEMPTS:
|
|
268
|
+
raise EnvironmentError(
|
|
269
|
+
"downloaded OCI blob failed digest verification "
|
|
270
|
+
f"after {attempt} attempts (expected {digest}, got "
|
|
271
|
+
f"{observed_digest}, bytes {written}/{size})"
|
|
272
|
+
)
|
|
273
|
+
events.emit(
|
|
274
|
+
"library.layer_download_retry",
|
|
275
|
+
"Retrying OCI blob after integrity verification failed",
|
|
276
|
+
digest=digest,
|
|
277
|
+
attempt=attempt + 1,
|
|
278
|
+
observed_digest=observed_digest,
|
|
279
|
+
downloaded_bytes=written,
|
|
280
|
+
expected_bytes=size,
|
|
281
|
+
)
|
|
282
|
+
except urllib.error.HTTPError as exc:
|
|
283
|
+
raise DownloadUnavailable(f"OCI blob download failed: HTTP {exc.code}") from exc
|
|
284
|
+
except OSError as exc:
|
|
285
|
+
raise DownloadUnavailable(f"OCI blob download failed: {exc}") from exc
|
|
264
286
|
return destination
|
|
265
287
|
|
|
266
288
|
def blob_exists(self, digest: str) -> bool:
|
|
@@ -34,6 +34,7 @@ def _progress(event: RuntimeEvent) -> None:
|
|
|
34
34
|
"package_reference.started": "Resolving dependency",
|
|
35
35
|
"library.lookup": "Looking for a cached environment",
|
|
36
36
|
"library.layer_download_started": "Downloading cached environment",
|
|
37
|
+
"library.layer_download_retry": "Retrying cached-environment download",
|
|
37
38
|
"environment.build_started": "Building environment",
|
|
38
39
|
"environment.cache_hit": "Using local environment",
|
|
39
40
|
}
|
|
@@ -314,14 +315,18 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
314
315
|
runtime_events=runtime_events,
|
|
315
316
|
).discover_and_check(source)
|
|
316
317
|
if discovered.status != "found" or discovered.execution_result is None:
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
rejection = discovered.rejection_attempt
|
|
319
|
+
if rejection is None or rejection.execution_result is None:
|
|
320
|
+
raise SpecificationError(_discovery_failure(discovered)) from None
|
|
321
|
+
result = rejection.execution_result
|
|
322
|
+
else:
|
|
323
|
+
if arguments.lock_out is not None:
|
|
324
|
+
assert discovered.lock is not None
|
|
325
|
+
discovered.lock.write(arguments.lock_out)
|
|
326
|
+
result = discovered.execution_result
|
|
321
327
|
preparation = PhaseTiming(
|
|
322
328
|
"discovery", round((time.monotonic() - discovery_started) * 1000)
|
|
323
329
|
)
|
|
324
|
-
result = discovered.execution_result
|
|
325
330
|
if context.lock is not None or context.toolchain is not None or not context.requires:
|
|
326
331
|
result = replace(result, timings=(preparation, *result.timings))
|
|
327
332
|
_emit(
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"required": ["phase", "duration_ms", "performed"],
|
|
30
30
|
"additionalProperties": false,
|
|
31
31
|
"properties": {
|
|
32
|
-
"phase": {"enum": ["toolchain", "resolution", "source_acquisition", "cache_lookup", "cache_download", "artifact_hydration", "build", "publication", "environment_open", "instance_creation", "input_staging", "execution", "result_collection", "result_publication", "cleanup"]},
|
|
32
|
+
"phase": {"enum": ["discovery", "toolchain", "resolution", "source_acquisition", "cache_lookup", "cache_download", "artifact_hydration", "build", "publication", "environment_open", "instance_creation", "input_staging", "execution", "result_collection", "result_publication", "cleanup"]},
|
|
33
33
|
"duration_ms": {"type": "integer", "minimum": 0},
|
|
34
34
|
"performed": {"type": "boolean"}
|
|
35
35
|
}
|
|
@@ -7,7 +7,13 @@ from dataclasses import dataclass, field
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
9
|
from lean_runtime import ExecutionResult, Runtime
|
|
10
|
-
from lean_runtime.discovery import
|
|
10
|
+
from lean_runtime.discovery import (
|
|
11
|
+
Discovery,
|
|
12
|
+
DiscoveryError,
|
|
13
|
+
DiscoveryPolicy,
|
|
14
|
+
PolicyError,
|
|
15
|
+
ProbeOutcome,
|
|
16
|
+
)
|
|
11
17
|
from lean_runtime.discovery.candidate import Candidate
|
|
12
18
|
from lean_runtime.discovery.probe import ProbeIntegrityFailure, ProbeUnavailable
|
|
13
19
|
|
|
@@ -87,6 +93,25 @@ def test_candidate_budget_never_opens_bounded_candidate(sample_catalog) -> None:
|
|
|
87
93
|
assert probe.opened == ["mathlib-new"]
|
|
88
94
|
|
|
89
95
|
|
|
96
|
+
def test_exhaustion_surfaces_the_highest_ranked_lean_rejection(sample_catalog) -> None: # type: ignore[no-untyped-def]
|
|
97
|
+
probe = FakeProbe({"mathlib-new": execution(False, stderr="type mismatch")})
|
|
98
|
+
result = Discovery(
|
|
99
|
+
catalog=sample_catalog,
|
|
100
|
+
policy=DiscoveryPolicy(max_candidates=1),
|
|
101
|
+
probe=probe,
|
|
102
|
+
).discover_and_check("import Mathlib\n")
|
|
103
|
+
|
|
104
|
+
assert result.status == "not_found"
|
|
105
|
+
assert result.rejection_attempt is result.attempts[0]
|
|
106
|
+
assert [item.code for item in result.diagnostics] == [
|
|
107
|
+
"DISCOVERY_CANDIDATE_LIMIT",
|
|
108
|
+
"CANDIDATE_LEAN_REJECTED",
|
|
109
|
+
]
|
|
110
|
+
assert result.diagnostics[1].detail == "type mismatch"
|
|
111
|
+
with pytest.raises(DiscoveryError, match="type mismatch"):
|
|
112
|
+
result.raise_for_error()
|
|
113
|
+
|
|
114
|
+
|
|
90
115
|
def test_unavailable_candidate_advances(sample_catalog) -> None: # type: ignore[no-untyped-def]
|
|
91
116
|
probe = FakeProbe(
|
|
92
117
|
{
|
|
@@ -18,8 +18,14 @@ import pytest
|
|
|
18
18
|
from lean_runtime import EnvironmentError, EnvironmentLock, LockedPackage, Runtime
|
|
19
19
|
from lean_runtime.bundles import _extract_layer, _oci_archive
|
|
20
20
|
from lean_runtime.environments import ENVIRONMENT_SCHEMA
|
|
21
|
-
from lean_runtime.
|
|
22
|
-
from lean_runtime.
|
|
21
|
+
from lean_runtime.events import EventEmitter
|
|
22
|
+
from lean_runtime.oci import OCIEnvironmentPublisher, OCIRegistryClient, OCIRepository
|
|
23
|
+
from lean_runtime.store import (
|
|
24
|
+
EnvironmentStore,
|
|
25
|
+
environment_identity,
|
|
26
|
+
platform_record,
|
|
27
|
+
source_snapshot_digest,
|
|
28
|
+
)
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
def _git_package(path: Path) -> tuple[str, str]:
|
|
@@ -403,6 +409,50 @@ def test_transparent_authenticated_oci_pull_and_blob_reuse(tmp_path: Path) -> No
|
|
|
403
409
|
thread.join(timeout=5)
|
|
404
410
|
|
|
405
411
|
|
|
412
|
+
def test_oci_blob_integrity_failure_retries_once_from_scratch(tmp_path: Path) -> None:
|
|
413
|
+
data = b"verified layer contents"
|
|
414
|
+
digest = "sha256:" + hashlib.sha256(data).hexdigest()
|
|
415
|
+
requests = 0
|
|
416
|
+
cache_controls: list[str | None] = []
|
|
417
|
+
|
|
418
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
419
|
+
def do_GET(self) -> None:
|
|
420
|
+
nonlocal requests
|
|
421
|
+
requests += 1
|
|
422
|
+
cache_controls.append(self.headers.get("Cache-Control"))
|
|
423
|
+
response = b"x" * len(data) if requests == 1 else data
|
|
424
|
+
self.send_response(200)
|
|
425
|
+
self.send_header("Content-Length", str(len(response)))
|
|
426
|
+
self.end_headers()
|
|
427
|
+
self.wfile.write(response)
|
|
428
|
+
|
|
429
|
+
def log_message(self, _format: str, *_args: object) -> None:
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
433
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
434
|
+
thread.start()
|
|
435
|
+
events = []
|
|
436
|
+
try:
|
|
437
|
+
repository = OCIRepository.parse(f"oci+http://127.0.0.1:{server.server_port}/owner/cache")
|
|
438
|
+
path = OCIRegistryClient(repository).download_blob(
|
|
439
|
+
{"digest": digest, "size": len(data)},
|
|
440
|
+
EnvironmentStore(tmp_path / "consumer"),
|
|
441
|
+
EventEmitter(events.append),
|
|
442
|
+
)
|
|
443
|
+
assert path.read_bytes() == data
|
|
444
|
+
assert requests == 2
|
|
445
|
+
assert cache_controls == [None, "no-cache"]
|
|
446
|
+
assert [event.kind for event in events].count("library.layer_download_retry") == 1
|
|
447
|
+
starts = [event for event in events if event.kind == "library.layer_download_started"]
|
|
448
|
+
assert [event.data["attempt"] for event in starts] == [1, 2]
|
|
449
|
+
assert starts[1].data["resumed_bytes"] == 0
|
|
450
|
+
finally:
|
|
451
|
+
server.shutdown()
|
|
452
|
+
server.server_close()
|
|
453
|
+
thread.join(timeout=5)
|
|
454
|
+
|
|
455
|
+
|
|
406
456
|
def test_oci_publisher_uploads_blobs_before_manifests(tmp_path: Path) -> None:
|
|
407
457
|
producer, environment_id, lock = _published_runtime(tmp_path / "producer")
|
|
408
458
|
publisher = OCIEnvironmentPublisher(
|
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
import pytest
|
|
10
10
|
|
|
11
|
-
from lean_runtime import EnvironmentLock, ExecutionPolicy
|
|
11
|
+
from lean_runtime import EnvironmentError, EnvironmentLock, ExecutionPolicy
|
|
12
12
|
from lean_runtime.backends import LocalBackend
|
|
13
13
|
from lean_runtime.environments import Environment, EnvironmentManager
|
|
14
14
|
from lean_runtime.store import EnvironmentStore
|
|
@@ -79,9 +79,7 @@ def test_interactive_session_round_trips_and_records_transcript(tmp_path: Path)
|
|
|
79
79
|
execution_id = session.execution_id
|
|
80
80
|
assert session.running
|
|
81
81
|
assert session.poll() is None
|
|
82
|
-
session.
|
|
83
|
-
session.stdin.flush()
|
|
84
|
-
assert session.stdout.readline() == "HELLO LEAN\n"
|
|
82
|
+
assert session.request_line("hello lean") == "HELLO LEAN"
|
|
85
83
|
assert any(environment.manager.store.jobs.iterdir())
|
|
86
84
|
|
|
87
85
|
result = session.close()
|
|
@@ -97,6 +95,23 @@ def test_interactive_session_round_trips_and_records_transcript(tmp_path: Path)
|
|
|
97
95
|
assert record["operation"] == "interactive"
|
|
98
96
|
|
|
99
97
|
|
|
98
|
+
def test_interactive_session_round_trips_ndjson(tmp_path: Path) -> None:
|
|
99
|
+
environment = _environment(tmp_path)
|
|
100
|
+
script = (
|
|
101
|
+
"import json, sys\n"
|
|
102
|
+
"for line in sys.stdin:\n"
|
|
103
|
+
" value = json.loads(line)\n"
|
|
104
|
+
" print(json.dumps({'id': value['id'], 'answer': value['value'] + 1}), flush=True)"
|
|
105
|
+
)
|
|
106
|
+
with environment.spawn_interactive([sys.executable, "-u", "-c", script]) as session:
|
|
107
|
+
assert session.request_json({"id": 7, "value": 41}) == {"id": 7, "answer": 42}
|
|
108
|
+
with pytest.raises(ValueError, match="exactly one line"):
|
|
109
|
+
session.send_line("first\nsecond")
|
|
110
|
+
|
|
111
|
+
with pytest.raises(EnvironmentError, match="not running"):
|
|
112
|
+
session.send_line("late request")
|
|
113
|
+
|
|
114
|
+
|
|
100
115
|
def test_interactive_timeout_is_enforced_and_instance_is_removed(tmp_path: Path) -> None:
|
|
101
116
|
environment = _environment(tmp_path)
|
|
102
117
|
session = environment.spawn_interactive(
|
|
@@ -34,9 +34,7 @@ def test_program_identity_payload_validation_and_interactive_execution(tmp_path:
|
|
|
34
34
|
|
|
35
35
|
assert program.id.startswith("program_")
|
|
36
36
|
with program.spawn_interactive(policy=ExecutionPolicy(timeout_seconds=10)) as session:
|
|
37
|
-
session.
|
|
38
|
-
session.stdin.flush()
|
|
39
|
-
assert session.stdout.readline() == '{"ping":true}\n'
|
|
37
|
+
assert session.request_json({"ping": True}) == {"ping": True}
|
|
40
38
|
assert session.close().provenance is not None
|
|
41
39
|
assert session.close().provenance.program_id == program.id
|
|
42
40
|
|
|
@@ -1,20 +1,27 @@
|
|
|
1
|
+
import json
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
from lean_runtime import ProjectError
|
|
4
|
-
from lean_runtime.models import ExecutionResult
|
|
5
|
+
from lean_runtime.models import Diagnostic, ExecutionResult
|
|
5
6
|
from lean_runtime.run_cli import main
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
def _result(
|
|
9
|
+
def _result(
|
|
10
|
+
*,
|
|
11
|
+
ok: bool = True,
|
|
12
|
+
stderr: str = "",
|
|
13
|
+
diagnostics: tuple[Diagnostic, ...] = (),
|
|
14
|
+
) -> ExecutionResult:
|
|
9
15
|
return ExecutionResult(
|
|
10
|
-
ok=
|
|
11
|
-
exit_code=0,
|
|
16
|
+
ok=ok,
|
|
17
|
+
exit_code=0 if ok else 1,
|
|
12
18
|
toolchain="leanprover/lean4:v4.32.0",
|
|
13
19
|
command=("lean", "Main.lean"),
|
|
14
20
|
cwd="/tmp",
|
|
15
21
|
stdout="",
|
|
16
|
-
stderr=
|
|
22
|
+
stderr=stderr,
|
|
17
23
|
elapsed_seconds=0.01,
|
|
24
|
+
diagnostics=diagnostics,
|
|
18
25
|
)
|
|
19
26
|
|
|
20
27
|
|
|
@@ -158,6 +165,65 @@ def test_lean_run_discovers_standalone_file_and_writes_lock(
|
|
|
158
165
|
assert "accepted" in capsys.readouterr().out
|
|
159
166
|
|
|
160
167
|
|
|
168
|
+
def test_lean_run_preserves_discovered_compiler_rejection(
|
|
169
|
+
monkeypatch, tmp_path: Path, capsys
|
|
170
|
+
) -> None:
|
|
171
|
+
source = tmp_path / "Main.lean"
|
|
172
|
+
source.write_text("import Mathlib\nexample : False := by trivial\n")
|
|
173
|
+
rejection = _result(
|
|
174
|
+
ok=False,
|
|
175
|
+
stderr="Main.lean:2:22: error: tactic 'trivial' failed",
|
|
176
|
+
diagnostics=(
|
|
177
|
+
Diagnostic(
|
|
178
|
+
"error",
|
|
179
|
+
"tactic 'trivial' failed",
|
|
180
|
+
file="Main.lean",
|
|
181
|
+
line=2,
|
|
182
|
+
column=22,
|
|
183
|
+
),
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
class NoProjectRuntime(FakeRuntime):
|
|
188
|
+
def __init__(self, **kwargs) -> None:
|
|
189
|
+
super().__init__(**kwargs)
|
|
190
|
+
self.availability = "auto"
|
|
191
|
+
self.libraries = ()
|
|
192
|
+
|
|
193
|
+
def check_file(self, *_args, **_kwargs) -> ExecutionResult:
|
|
194
|
+
raise ProjectError("no project")
|
|
195
|
+
|
|
196
|
+
class Attempt:
|
|
197
|
+
execution_result = rejection
|
|
198
|
+
|
|
199
|
+
class Rejected:
|
|
200
|
+
status = "not_found"
|
|
201
|
+
execution_result = None
|
|
202
|
+
rejection_attempt = Attempt()
|
|
203
|
+
|
|
204
|
+
class FakeDiscovery:
|
|
205
|
+
def __init__(self, **_kwargs) -> None:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
def discover_and_check(self, _source: str) -> Rejected:
|
|
209
|
+
return Rejected()
|
|
210
|
+
|
|
211
|
+
monkeypatch.setattr("lean_runtime.run_cli.Runtime", NoProjectRuntime)
|
|
212
|
+
monkeypatch.setattr("lean_runtime.run_cli.Discovery", FakeDiscovery)
|
|
213
|
+
|
|
214
|
+
assert main([str(source), "--quiet"]) == 1
|
|
215
|
+
captured = capsys.readouterr()
|
|
216
|
+
assert "tactic 'trivial' failed" in captured.err
|
|
217
|
+
assert "rejected" in captured.out
|
|
218
|
+
|
|
219
|
+
assert main([str(source), "--json", "--quiet"]) == 1
|
|
220
|
+
payload = json.loads(capsys.readouterr().out)
|
|
221
|
+
assert payload["ok"] is False
|
|
222
|
+
assert payload["data"]["stderr"].endswith("tactic 'trivial' failed")
|
|
223
|
+
assert payload["data"]["diagnostics"][0]["message"] == "tactic 'trivial' failed"
|
|
224
|
+
assert payload["data"]["timings"][0]["phase"] == "discovery"
|
|
225
|
+
|
|
226
|
+
|
|
161
227
|
def test_lean_run_can_disable_automatic_discovery(monkeypatch, tmp_path: Path, capsys) -> None:
|
|
162
228
|
source = tmp_path / "Main.lean"
|
|
163
229
|
source.write_text("import Mathlib\n")
|
|
@@ -58,7 +58,7 @@ def test_execution_success_fixture_matches_v1_schema() -> None:
|
|
|
58
58
|
stdout="",
|
|
59
59
|
stderr="",
|
|
60
60
|
elapsed_seconds=0.01,
|
|
61
|
-
timings=(PhaseTiming("execution", 10)
|
|
61
|
+
timings=(PhaseTiming("discovery", 5), PhaseTiming("execution", 10)),
|
|
62
62
|
)
|
|
63
63
|
_validator("execution-v1.schema.json").validate(serialize_execution_v1(result))
|
|
64
64
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/schemas/catalog-v1.schema.json
RENAMED
|
File without changes
|
{lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/schemas/plan-v1.schema.json
RENAMED
|
File without changes
|
{lean_runtime-2.0.5 → lean_runtime-2.0.7}/lean_runtime/discovery/schemas/result-v1.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lean_runtime-2.0.5 → lean_runtime-2.0.7}/tests/discovery/test_discovery_schema_resources.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|