fastws-cli 0.0.33__tar.gz → 0.0.34__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.
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/PKG-INFO +36 -5
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/README.md +35 -4
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws/__init__.py +2 -1
- fastws_cli-0.0.34/fastws/_toml.py +4 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws/cargo.py +77 -56
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws/core.py +22 -13
- fastws_cli-0.0.34/fastws/integrate.py +136 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws/store.py +6 -2
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws_cli.egg-info/PKG-INFO +36 -5
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws_cli.egg-info/SOURCES.txt +3 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/tests/test_build.py +26 -5
- fastws_cli-0.0.34/tests/test_integrate.py +46 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/LICENSE +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws/build_backend.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws/news.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws/procs.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws/releases.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws/timelock.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws_cli.egg-info/dependency_links.txt +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws_cli.egg-info/entry_points.txt +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws_cli.egg-info/requires.txt +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/fastws_cli.egg-info/top_level.txt +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/pyproject.toml +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/setup.cfg +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/tests/test_build_backend.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/tests/test_news.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/tests/test_procs.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/tests/test_releases.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/tests/test_setup.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/tests/test_store.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/tests/test_sync.py +0 -0
- {fastws_cli-0.0.33 → fastws_cli-0.0.34}/tests/test_timelock.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastws-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.34
|
|
4
4
|
Summary: Fast workspace tools for multi-repo management
|
|
5
5
|
Author: Jeremy Howard
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -205,9 +205,10 @@ Build a mixed Rust/Python project's extension and opted-in binaries, keep them w
|
|
|
205
205
|
```bash
|
|
206
206
|
cargo develop
|
|
207
207
|
cargo develop --path /path/to/project
|
|
208
|
+
cargo develop --profile quick
|
|
208
209
|
```
|
|
209
210
|
|
|
210
|
-
Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`.
|
|
211
|
+
Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`. `[tool.fastws] dev-profile = "quick"` selects a project's default for both `cargo develop` and `fastws.cargo.run_tests`. Without that setting they use `test`. An explicit `--profile NAME` overrides the project default. The project must produce a `cdylib` and set `python-source`. Define the selected profile in `Cargo.toml` and keep incremental compilation enabled for the local crate.
|
|
211
212
|
|
|
212
213
|
Make the initial editable installation through `ws-sync` or maturin. Repeat that installation when package metadata or entry points change. Running Python processes keep the extension they loaded.
|
|
213
214
|
|
|
@@ -253,11 +254,11 @@ cargo wait --path /path/to/workspace
|
|
|
253
254
|
|
|
254
255
|
Use it when another process's unfinished edits stop the workspace compiling, such as a second agent working in the same tree. Each check runs `cargo check --all-targets`, whose rustc calls hold the timing lock shared, as every build's do. After a failed check, `cargo wait` prints Cargo's first error line. It checks again once a source file of a local package changes and then stays unchanged for 2 seconds. Local packages are the workspace's members and its path dependencies, such as a sibling crate that `ws-sync` patches in. When a check passes, it prints `compiles` and exits. After `--patience` seconds, 1800 by default, it prints the last check's errors and exits with status 1.
|
|
255
256
|
|
|
256
|
-
### Rust tests from Python
|
|
257
|
+
### Rust builds and tests from Python
|
|
257
258
|
|
|
258
|
-
`fastws.cargo.run_tests` builds native tests once with Cargo, without a time limit, then runs the executables that Cargo reports directly from their package directories.
|
|
259
|
+
`fastws.cargo.run_tests` builds native tests once with Cargo, without a time limit, then runs the executables that Cargo reports directly from their package directories. Each binary gets its own `timeout`, 30 seconds by default, excluding compilation. A binary still running when its `timeout` ends is killed and counts as a failed binary, with its suite's `timed_out` set. Source edits between building and running do not trigger another build: the results describe the compiled build, not necessarily the latest working tree. It supports directly runnable native tests, not cross-target runners.
|
|
259
260
|
|
|
260
|
-
Pass Cargo build/selection options positionally, and test filters and harness options in `test_args`. A failed binary stops the run unless `--no-fail-fast` is given. Results contain compiler diagnostics and each binary's outcomes and captured failures.
|
|
261
|
+
The default profile is `[tool.fastws].dev-profile`, or `test` when unset. Explicit `--profile NAME`, `--profile=NAME`, `--release` and `-r` override it. Pass Cargo build/selection options positionally, and test filters and harness options in `test_args`. A failed binary stops the run unless `--no-fail-fast` is given. Results contain compiler diagnostics and each binary's outcomes and captured failures.
|
|
261
262
|
|
|
262
263
|
```python
|
|
263
264
|
from fastws.cargo import run_tests
|
|
@@ -268,6 +269,34 @@ focused = run_tests('.', '--test', 'core', test_args=('test_name', '--nocapture'
|
|
|
268
269
|
|
|
269
270
|
Doctests are deliberately not run; keep executable examples in ordinary Rust tests. `run_tests` accepts `env` overrides and `timeout=None` to disable its watchdog. It takes no timing lock around the run: rustc's wrapper owns its shared holds, and tests that time own their exclusive holds.
|
|
270
271
|
|
|
272
|
+
`fastws.cargo.check` runs any Cargo subcommand, `check --all-targets` by default, and returns the same `Results` without test suites. A failing run is a result, not an exception. Each `Diagnostic` in its `diagnostics` holds the message's path, line, column, level and text, and its error code or lint as `code`, such as `E0308` or `clippy::too_many_lines`. The summary lists errors first. `env` sets environment variables for the run, as for `run_tests`. A subcommand that rejects `--message-format`, such as `cargo llvm-lines`, runs with `diagnostics=False`: its result holds the exit status and output, with no diagnostics.
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from fastws.cargo import check
|
|
276
|
+
|
|
277
|
+
lints = check('.', 'clippy', '--all-targets', '--', '-W', 'clippy::too_many_lines', env={'CLIPPY_CONF_DIR': 'review'})
|
|
278
|
+
errors = [d for d in lints.diagnostics if d.level == 'error']
|
|
279
|
+
sizes = check('.', 'llvm-lines', '--lib', diagnostics=False).text
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Integration snapshots
|
|
283
|
+
|
|
284
|
+
`fastws.integrate` builds several repos together while people or agents keep editing them. `integrate` snapshots each repo's working tree as a commit (untracked files included, ignored files not), checks each commit out as a detached worktree under one root, and writes the root's Cargo config. `HEAD` and the index of each live repo don't change. The config is the workspace's, with the patches for crates under the root pointing at the root's worktrees, so a build there reads the snapshots and every other local crate from the workspace. Keep the root outside the workspace, where `ws-sync` and uv don't treat its worktrees as members.
|
|
285
|
+
|
|
286
|
+
`errors` checks each snapshot in turn and keeps the errors in that repo's own files. A repo's check also reports its dependencies' errors, under paths outside the repo, and each dependency's own check already covers those. `owners` reads each owner's path globs from the bullets under a Markdown file's `Files` heading, one ``- Owner: `glob` `glob` `` line each, in order. `by_owner` groups each repo's compiler messages by owner, then by file, and the first owner whose globs match a path owns it. It prefixes each path with its repo's name, so a path into a sibling repo, such as `../rustymath/src/x.rs` from basedpl's check, groups as `rustymath/src/x.rs`. `run_tests` with `--no-fail-fast` runs every test binary, each within its own `timeout`, so a slow or crashing binary doesn't stop the others. `code_count` counts the Rust code lines in a sequence of lines, leaving out blanks and comments. `code_lines` applies it to each Rust file under a directory. `exhash`'s `Journal.stat(count=code_count)` counts the code lines that edits add and remove. `record` writes a run's messages, each repo's test `Results` with each failure's output, and any sizes, as a Markdown record, and returns each owner's message count.
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from fastws.cargo import run_tests
|
|
290
|
+
from fastws.integrate import by_owner, code_lines, errors, integrate, owners, record
|
|
291
|
+
|
|
292
|
+
root = '~/git/integration'
|
|
293
|
+
commits = integrate({'basedpl': '~/aai-ws/basedpl', 'rustymath': '~/aai-ws/rustymath'}, root)
|
|
294
|
+
errs = errors(root, ['rustymath', 'basedpl'])
|
|
295
|
+
groups = by_owner(errs, owners('~/aai-ws/basedpl/meta/owners.md'))
|
|
296
|
+
tests = {r: run_tests(f'{root}/{r}', '--no-fail-fast') for r in errs if not errs[r]}
|
|
297
|
+
sizes = {r: code_lines(f'{root}/{r}/src') for r in errs}
|
|
298
|
+
counts = record(f'{root}/run8.md', 'Run 8', commits, groups, tests=tests, sizes=sizes)
|
|
299
|
+
```
|
|
271
300
|
### `ws-sync`
|
|
272
301
|
|
|
273
302
|
Syncing proceeds in this order:
|
|
@@ -293,6 +322,8 @@ Do not commit a `Cargo.lock` generated under these patches. Its source-less loca
|
|
|
293
322
|
|
|
294
323
|
Sync writes `.cargo/rustc-lock` and makes it Cargo's `rustc-wrapper`. The script runs each rustc call holding the timing lock shared (see `cargo develop` above). Inside the lock it runs the wrapper that the config named before sync first wrote the script, where another tool set one, and otherwise `sccache` when it's installed, so unchanged compilation units are reused across the workspace's otherwise-independent Cargo target directories. Later syncs keep that inner wrapper. The script costs about 9 ms a rustc call, mostly the Python interpreter's start. If `sccache` isn't installed, sync warns with the install command (`cargo install sccache --locked`) and continues; rerun `ws-sync` after installing to enable it. On Windows, where the lock does nothing, the config keeps another tool's wrapper, or else names `sccache`.
|
|
295
324
|
|
|
325
|
+
For an isolated build workspace, `fastws.core.cargo_setup(root)` regenerates these Cargo patches and the same compiler wrapper without pulling, syncing Python or building.
|
|
326
|
+
|
|
296
327
|
Fastws discovers JavaScript packages directly under the workspace root. It reads each package's `workspaces` list for declared nested packages. These lists accept paths and globs. Fastws ignores undeclared nested packages. Lockfiles do not affect discovery. `[tool.fastws].exclude` matches paths relative to the workspace root. Fastws skips `node_modules` and directories whose names start with `.` or `_`.
|
|
297
328
|
|
|
298
329
|
Each sync updates `workspaces` in the root `package.json`. It creates the file when needed. It preserves external paths, globs, and unrelated settings. It reports added and removed packages.
|
|
@@ -177,9 +177,10 @@ Build a mixed Rust/Python project's extension and opted-in binaries, keep them w
|
|
|
177
177
|
```bash
|
|
178
178
|
cargo develop
|
|
179
179
|
cargo develop --path /path/to/project
|
|
180
|
+
cargo develop --profile quick
|
|
180
181
|
```
|
|
181
182
|
|
|
182
|
-
Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`.
|
|
183
|
+
Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`. `[tool.fastws] dev-profile = "quick"` selects a project's default for both `cargo develop` and `fastws.cargo.run_tests`. Without that setting they use `test`. An explicit `--profile NAME` overrides the project default. The project must produce a `cdylib` and set `python-source`. Define the selected profile in `Cargo.toml` and keep incremental compilation enabled for the local crate.
|
|
183
184
|
|
|
184
185
|
Make the initial editable installation through `ws-sync` or maturin. Repeat that installation when package metadata or entry points change. Running Python processes keep the extension they loaded.
|
|
185
186
|
|
|
@@ -225,11 +226,11 @@ cargo wait --path /path/to/workspace
|
|
|
225
226
|
|
|
226
227
|
Use it when another process's unfinished edits stop the workspace compiling, such as a second agent working in the same tree. Each check runs `cargo check --all-targets`, whose rustc calls hold the timing lock shared, as every build's do. After a failed check, `cargo wait` prints Cargo's first error line. It checks again once a source file of a local package changes and then stays unchanged for 2 seconds. Local packages are the workspace's members and its path dependencies, such as a sibling crate that `ws-sync` patches in. When a check passes, it prints `compiles` and exits. After `--patience` seconds, 1800 by default, it prints the last check's errors and exits with status 1.
|
|
227
228
|
|
|
228
|
-
### Rust tests from Python
|
|
229
|
+
### Rust builds and tests from Python
|
|
229
230
|
|
|
230
|
-
`fastws.cargo.run_tests` builds native tests once with Cargo, without a time limit, then runs the executables that Cargo reports directly from their package directories.
|
|
231
|
+
`fastws.cargo.run_tests` builds native tests once with Cargo, without a time limit, then runs the executables that Cargo reports directly from their package directories. Each binary gets its own `timeout`, 30 seconds by default, excluding compilation. A binary still running when its `timeout` ends is killed and counts as a failed binary, with its suite's `timed_out` set. Source edits between building and running do not trigger another build: the results describe the compiled build, not necessarily the latest working tree. It supports directly runnable native tests, not cross-target runners.
|
|
231
232
|
|
|
232
|
-
Pass Cargo build/selection options positionally, and test filters and harness options in `test_args`. A failed binary stops the run unless `--no-fail-fast` is given. Results contain compiler diagnostics and each binary's outcomes and captured failures.
|
|
233
|
+
The default profile is `[tool.fastws].dev-profile`, or `test` when unset. Explicit `--profile NAME`, `--profile=NAME`, `--release` and `-r` override it. Pass Cargo build/selection options positionally, and test filters and harness options in `test_args`. A failed binary stops the run unless `--no-fail-fast` is given. Results contain compiler diagnostics and each binary's outcomes and captured failures.
|
|
233
234
|
|
|
234
235
|
```python
|
|
235
236
|
from fastws.cargo import run_tests
|
|
@@ -240,6 +241,34 @@ focused = run_tests('.', '--test', 'core', test_args=('test_name', '--nocapture'
|
|
|
240
241
|
|
|
241
242
|
Doctests are deliberately not run; keep executable examples in ordinary Rust tests. `run_tests` accepts `env` overrides and `timeout=None` to disable its watchdog. It takes no timing lock around the run: rustc's wrapper owns its shared holds, and tests that time own their exclusive holds.
|
|
242
243
|
|
|
244
|
+
`fastws.cargo.check` runs any Cargo subcommand, `check --all-targets` by default, and returns the same `Results` without test suites. A failing run is a result, not an exception. Each `Diagnostic` in its `diagnostics` holds the message's path, line, column, level and text, and its error code or lint as `code`, such as `E0308` or `clippy::too_many_lines`. The summary lists errors first. `env` sets environment variables for the run, as for `run_tests`. A subcommand that rejects `--message-format`, such as `cargo llvm-lines`, runs with `diagnostics=False`: its result holds the exit status and output, with no diagnostics.
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from fastws.cargo import check
|
|
248
|
+
|
|
249
|
+
lints = check('.', 'clippy', '--all-targets', '--', '-W', 'clippy::too_many_lines', env={'CLIPPY_CONF_DIR': 'review'})
|
|
250
|
+
errors = [d for d in lints.diagnostics if d.level == 'error']
|
|
251
|
+
sizes = check('.', 'llvm-lines', '--lib', diagnostics=False).text
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Integration snapshots
|
|
255
|
+
|
|
256
|
+
`fastws.integrate` builds several repos together while people or agents keep editing them. `integrate` snapshots each repo's working tree as a commit (untracked files included, ignored files not), checks each commit out as a detached worktree under one root, and writes the root's Cargo config. `HEAD` and the index of each live repo don't change. The config is the workspace's, with the patches for crates under the root pointing at the root's worktrees, so a build there reads the snapshots and every other local crate from the workspace. Keep the root outside the workspace, where `ws-sync` and uv don't treat its worktrees as members.
|
|
257
|
+
|
|
258
|
+
`errors` checks each snapshot in turn and keeps the errors in that repo's own files. A repo's check also reports its dependencies' errors, under paths outside the repo, and each dependency's own check already covers those. `owners` reads each owner's path globs from the bullets under a Markdown file's `Files` heading, one ``- Owner: `glob` `glob` `` line each, in order. `by_owner` groups each repo's compiler messages by owner, then by file, and the first owner whose globs match a path owns it. It prefixes each path with its repo's name, so a path into a sibling repo, such as `../rustymath/src/x.rs` from basedpl's check, groups as `rustymath/src/x.rs`. `run_tests` with `--no-fail-fast` runs every test binary, each within its own `timeout`, so a slow or crashing binary doesn't stop the others. `code_count` counts the Rust code lines in a sequence of lines, leaving out blanks and comments. `code_lines` applies it to each Rust file under a directory. `exhash`'s `Journal.stat(count=code_count)` counts the code lines that edits add and remove. `record` writes a run's messages, each repo's test `Results` with each failure's output, and any sizes, as a Markdown record, and returns each owner's message count.
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from fastws.cargo import run_tests
|
|
262
|
+
from fastws.integrate import by_owner, code_lines, errors, integrate, owners, record
|
|
263
|
+
|
|
264
|
+
root = '~/git/integration'
|
|
265
|
+
commits = integrate({'basedpl': '~/aai-ws/basedpl', 'rustymath': '~/aai-ws/rustymath'}, root)
|
|
266
|
+
errs = errors(root, ['rustymath', 'basedpl'])
|
|
267
|
+
groups = by_owner(errs, owners('~/aai-ws/basedpl/meta/owners.md'))
|
|
268
|
+
tests = {r: run_tests(f'{root}/{r}', '--no-fail-fast') for r in errs if not errs[r]}
|
|
269
|
+
sizes = {r: code_lines(f'{root}/{r}/src') for r in errs}
|
|
270
|
+
counts = record(f'{root}/run8.md', 'Run 8', commits, groups, tests=tests, sizes=sizes)
|
|
271
|
+
```
|
|
243
272
|
### `ws-sync`
|
|
244
273
|
|
|
245
274
|
Syncing proceeds in this order:
|
|
@@ -265,6 +294,8 @@ Do not commit a `Cargo.lock` generated under these patches. Its source-less loca
|
|
|
265
294
|
|
|
266
295
|
Sync writes `.cargo/rustc-lock` and makes it Cargo's `rustc-wrapper`. The script runs each rustc call holding the timing lock shared (see `cargo develop` above). Inside the lock it runs the wrapper that the config named before sync first wrote the script, where another tool set one, and otherwise `sccache` when it's installed, so unchanged compilation units are reused across the workspace's otherwise-independent Cargo target directories. Later syncs keep that inner wrapper. The script costs about 9 ms a rustc call, mostly the Python interpreter's start. If `sccache` isn't installed, sync warns with the install command (`cargo install sccache --locked`) and continues; rerun `ws-sync` after installing to enable it. On Windows, where the lock does nothing, the config keeps another tool's wrapper, or else names `sccache`.
|
|
267
296
|
|
|
297
|
+
For an isolated build workspace, `fastws.core.cargo_setup(root)` regenerates these Cargo patches and the same compiler wrapper without pulling, syncing Python or building.
|
|
298
|
+
|
|
268
299
|
Fastws discovers JavaScript packages directly under the workspace root. It reads each package's `workspaces` list for declared nested packages. These lists accept paths and globs. Fastws ignores undeclared nested packages. Lockfiles do not affect discovery. `[tool.fastws].exclude` matches paths relative to the workspace root. Fastws skips `node_modules` and directories whose names start with `.` or `_`.
|
|
269
300
|
|
|
270
301
|
Each sync updates `workspaces` in the root `package.json`. It creates the file when needed. It preserves external paths, globs, and unrelated settings. It reports added and removed packages.
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
r"Build, test and install Cargo artifacts for mixed Rust/Python projects."
|
|
2
|
-
import json, os, re, shutil, subprocess, sys, sysconfig, time
|
|
2
|
+
import json, os, re, shutil, signal, subprocess, sys, sysconfig, time
|
|
3
3
|
from collections import Counter
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import NamedTuple
|
|
7
7
|
from . import store
|
|
8
8
|
from .procs import bounded
|
|
9
|
-
|
|
10
|
-
try: import tomllib
|
|
11
|
-
except ModuleNotFoundError: import tomli as tomllib
|
|
9
|
+
from ._toml import tomllib
|
|
12
10
|
|
|
13
11
|
|
|
14
12
|
class ConfigError(ValueError):
|
|
@@ -20,9 +18,13 @@ def settings(root):
|
|
|
20
18
|
return tomllib.loads((Path(root).expanduser()/'pyproject.toml').read_text()).get('tool', {})
|
|
21
19
|
|
|
22
20
|
|
|
21
|
+
def _dev_profile(root):
|
|
22
|
+
return settings(root).get('fastws', {}).get('dev-profile', 'test') if (root/'pyproject.toml').exists() else 'test'
|
|
23
|
+
|
|
24
|
+
|
|
23
25
|
def cargo(root, *args):
|
|
24
|
-
"Run Cargo in the project and return its standard output."
|
|
25
|
-
return subprocess.run(['cargo', *args], cwd=Path(root).expanduser(),
|
|
26
|
+
"Run Cargo in the project and return its standard output. A failure raises `CalledProcessError`, holding Cargo's standard error."
|
|
27
|
+
return subprocess.run(['cargo', *args], cwd=Path(root).expanduser(), capture_output=True, text=True, check=True).stdout
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def members(root):
|
|
@@ -30,32 +32,39 @@ def members(root):
|
|
|
30
32
|
return {Path(p['manifest_path']) for p in json.loads(cargo(root, 'metadata', '--no-deps', '--format-version', '1'))['packages']}
|
|
31
33
|
|
|
32
34
|
|
|
33
|
-
def check(root):
|
|
34
|
-
"Check every target of the workspace with Cargo. Cargo's messages are in the result's `stderr`, which `diagnostics` reads."
|
|
35
|
-
return subprocess.run(['cargo', 'check', '--all-targets', '--message-format=short'], cwd=Path(root).expanduser(), stderr=subprocess.PIPE, text=True)
|
|
36
|
-
|
|
37
|
-
|
|
38
35
|
_TEST = re.compile(r'^test (.+) \.\.\. (ok|FAILED|ignored)')
|
|
39
36
|
_FAILURE = re.compile(r'^---- (.+) std(?:out|err) ----$')
|
|
40
|
-
_DIAGNOSTIC = re.compile(r'^(\S.*?):(\d+):(\d+): (warning|error)(?:\[\w+\])?: (.*)$')
|
|
41
37
|
|
|
42
38
|
|
|
43
39
|
class Diagnostic(NamedTuple):
|
|
44
|
-
"A compiler message, read from Cargo's
|
|
40
|
+
"A compiler message at its primary span, read from Cargo's JSON messages."
|
|
45
41
|
path:str; line:int; col:int; level:str; message:str
|
|
46
|
-
|
|
42
|
+
code:str|None=None # The error code or lint, such as `E0308` or `clippy::too_many_lines`
|
|
43
|
+
def __str__(self): return f'{self.level}{f"[{self.code}]" if self.code else ""}: {self.path}:{self.line}: {self.message}'
|
|
47
44
|
|
|
48
45
|
|
|
49
|
-
def diagnostics
|
|
50
|
-
"
|
|
51
|
-
|
|
46
|
+
def _run(root, args, env=None, diagnostics=True):
|
|
47
|
+
"Run the Cargo subcommand and options `args` in `root`, with the environment variables `env` set: its exit status, its output, each `Diagnostic` once, and Cargo's other JSON messages, such as artifacts. With `diagnostics`, Cargo writes JSON messages and the output renders each compiler message short; without, the output is as Cargo printed it."
|
|
48
|
+
flags = ('--message-format=json-diagnostic-short',) if diagnostics else ()
|
|
49
|
+
p = bounded(['cargo', args[0], *flags, *args[1:]], seconds=None, merge=True, cwd=root, env=os.environ | (env or {}))
|
|
50
|
+
if not diagnostics: return p.returncode, p.stdout, [], []
|
|
51
|
+
text, found, rest = [], [], []
|
|
52
|
+
for line in p.stdout.splitlines(keepends=True):
|
|
53
|
+
if not line.startswith('{'): text.append(line); continue
|
|
54
|
+
m = json.loads(line)
|
|
55
|
+
if m['reason'] != 'compiler-message': rest.append(m); continue
|
|
56
|
+
msg = m['message']
|
|
57
|
+
text.append(msg['rendered'])
|
|
58
|
+
if s := next((s for s in msg['spans'] if s['is_primary']), None):
|
|
59
|
+
found.append(Diagnostic(s['file_name'], s['line_start'], s['column_start'], msg['level'], msg['message'], (msg['code'] or {}).get('code')))
|
|
60
|
+
return p.returncode, ''.join(text), list(dict.fromkeys(found)), rest
|
|
52
61
|
|
|
53
62
|
|
|
54
63
|
def _lines(text, n=20, tail=False):
|
|
55
|
-
"Up to `n` lines of `text
|
|
64
|
+
"Up to `n` lines of `text` (None: all), indented, from its end if `tail`, with a count of the lines left out."
|
|
56
65
|
lines = text.splitlines()
|
|
66
|
+
if n is None or len(lines) <= n: return [f' {l}' for l in lines]
|
|
57
67
|
shown = [f' {l}' for l in (lines[-n:] if tail else lines[:n])]
|
|
58
|
-
if len(lines) <= n: return shown
|
|
59
68
|
more = f' … {len(lines)-n} more lines'
|
|
60
69
|
return [more, *shown] if tail else [*shown, more]
|
|
61
70
|
|
|
@@ -69,52 +78,69 @@ class Suite:
|
|
|
69
78
|
failures:dict # Each failing test's captured output
|
|
70
79
|
summary:str # libtest's `test result:` line, empty for a binary that crashed or has its own harness
|
|
71
80
|
failed:bool # Did the binary exit unsuccessfully?
|
|
81
|
+
timed_out:bool=False # Was the binary killed for running out of time?
|
|
72
82
|
|
|
73
83
|
@classmethod
|
|
74
|
-
def read(cls, name, lines, returncode):
|
|
75
|
-
"Read one binary's lines and exit status."
|
|
76
|
-
failures, current = {}, None
|
|
84
|
+
def read(cls, name, lines, returncode, timed_out=False):
|
|
85
|
+
"Read one binary's lines and exit status, and whether it was killed for running out of time."
|
|
86
|
+
failures, current, in_failures = {}, None, False
|
|
77
87
|
for line in lines:
|
|
78
|
-
if
|
|
79
|
-
elif
|
|
88
|
+
if line in ('successes:', 'failures:'): current, in_failures = None, line == 'failures:'
|
|
89
|
+
elif in_failures and (m := _FAILURE.match(line)): current = failures.setdefault(m[1], [])
|
|
80
90
|
elif current is not None: current.append(line)
|
|
81
91
|
return cls(name, '\n'.join(lines), {m[1]: m[2] for l in lines if (m := _TEST.match(l))},
|
|
82
92
|
{k: '\n'.join(v).strip('\n') for k,v in failures.items()},
|
|
83
|
-
next((l for l in lines if l.startswith('test result: ')), ''), bool(returncode))
|
|
93
|
+
next((l for l in lines if l.startswith('test result: ')), ''), bool(returncode), timed_out)
|
|
84
94
|
|
|
85
95
|
def __str__(self):
|
|
86
96
|
n = Counter(self.tests.values())
|
|
87
97
|
status = ', '.join(f'{n[o]} {w}' for o,w in [('ok', 'passed'), ('FAILED', 'failed'), ('ignored', 'ignored')] if n[o])
|
|
88
|
-
if self.failed and not n['FAILED']:
|
|
98
|
+
if self.failed and not n['FAILED']:
|
|
99
|
+
end = 'ran out of time' if self.timed_out else 'failed'
|
|
100
|
+
status = f'{status}, then {end}' if status else end
|
|
89
101
|
time = f' in {m[1]}' if (m := re.search(r'finished in (\S+)$', self.summary)) else ''
|
|
90
102
|
return f'{self.name}: {status or "ok"}{time}'
|
|
91
103
|
|
|
92
104
|
|
|
93
105
|
@dataclass
|
|
94
106
|
class Results:
|
|
95
|
-
"A
|
|
107
|
+
"A Cargo run's outcome, its compiler messages and, for tests, each native test binary's results. It displays as a summary: the outcome, the compiler messages with errors first, each binary that ran tests, and each failure's output."
|
|
108
|
+
command:str # The Cargo subcommand, such as `test` or `clippy`
|
|
96
109
|
returncode:int
|
|
97
110
|
text:str # The build's messages and each binary's merged standard output and standard error
|
|
98
111
|
diagnostics:list # Each `Diagnostic` from the build, before the first binary ran
|
|
99
|
-
suites:list # Each binary's `Suite`, in execution order
|
|
112
|
+
suites:list=field(default_factory=list) # Each test binary's `Suite`, in execution order
|
|
100
113
|
seconds:float=0 # Wall time spent executing native test binaries, excluding compilation
|
|
101
114
|
|
|
102
115
|
@property
|
|
103
116
|
def ok(self): return self.returncode == 0
|
|
104
117
|
|
|
105
|
-
def
|
|
106
|
-
|
|
118
|
+
def summary(self, lines:int|None=20):
|
|
119
|
+
"The outcome, compiler messages, each binary's result and each failure's output, with up to `lines` lines of each output (None: all)"
|
|
120
|
+
res = [f'cargo {self.command} passed' if self.ok else f'cargo {self.command} failed with exit status {self.returncode}']
|
|
107
121
|
diags = sorted(self.diagnostics, key=lambda d: d.level != 'error')
|
|
108
|
-
res += [str(d) for d in diags[:
|
|
109
|
-
if len(diags) >
|
|
122
|
+
res += [str(d) for d in diags[:lines]]
|
|
123
|
+
if lines is not None and len(diags) > lines: res.append(f'{len(diags)-lines} more compiler messages in `diagnostics`')
|
|
110
124
|
ran = [s for s in self.suites if s.tests or not s.summary]
|
|
111
125
|
for s in ran:
|
|
112
126
|
res.append(str(s))
|
|
113
|
-
for t,out in s.failures.items(): res += [f' FAILED {t}', *_lines(out)]
|
|
114
|
-
if s.failed and not s.failures: res += _lines(s.output, tail=True)
|
|
127
|
+
for t,out in s.failures.items(): res += [f' FAILED {t}', *_lines(out, lines)]
|
|
128
|
+
if s.failed and not s.failures: res += _lines(s.output, lines, tail=True)
|
|
115
129
|
if idle := len(self.suites) - len(ran): res.append(f'{idle} test binaries ran no tests')
|
|
116
|
-
if not (self.ok or self.suites or diags): res += _lines(self.text, tail=True)
|
|
130
|
+
if not (self.ok or self.suites or diags): res += _lines(self.text, lines, tail=True)
|
|
117
131
|
return '\n'.join(res)
|
|
132
|
+
__repr__ = summary
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def check(
|
|
136
|
+
root, # A Cargo package or workspace (expands `~`)
|
|
137
|
+
*args, # The Cargo subcommand and its options, such as `'clippy', '--all-targets', '--', '-W', 'clippy::pedantic'`; none: `check --all-targets`
|
|
138
|
+
env:dict=None, # Environment variables to set for the run, such as `{'CLIPPY_CONF_DIR': '/path/to/review'}`
|
|
139
|
+
diagnostics:bool=True, # Ask Cargo for JSON compiler messages? False runs a subcommand that rejects `--message-format`, such as `llvm-lines`, with no diagnostics
|
|
140
|
+
)->Results:
|
|
141
|
+
"Run a Cargo subcommand and return its `Results`, with each compiler message as a `Diagnostic`. A failing run is a result, not an exception."
|
|
142
|
+
args = args or ('check', '--all-targets')
|
|
143
|
+
return Results(args[0], *_run(Path(root).expanduser(), args, env, diagnostics)[:3])
|
|
118
144
|
|
|
119
145
|
|
|
120
146
|
def run_tests(
|
|
@@ -122,33 +148,28 @@ def run_tests(
|
|
|
122
148
|
*args, # Cargo build/selection options, such as `'--test', 'core'` or `'--profile', 'checked'`; `--no-fail-fast` runs every binary
|
|
123
149
|
test_args=(), # Arguments passed directly to each test binary, such as `('name', '--nocapture')`
|
|
124
150
|
env:dict=None, # Environment variables to set for the run, such as `{'BASEDPL_COSTS': '1'}`
|
|
125
|
-
timeout:float=30 # Seconds
|
|
151
|
+
timeout:float=30 # Seconds that each native test binary may run after building; None: no limit
|
|
126
152
|
)->Results:
|
|
127
|
-
"Build native tests once, with no time limit, then run Cargo's reported executables directly from their package directories. Source edits after building cannot trigger another build; results describe the compiled build, not the latest working tree. `timeout`
|
|
128
|
-
root, env = Path(root).expanduser().resolve(),
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if m['reason'] == 'compiler-message': text.append(m['message']['rendered'])
|
|
135
|
-
elif m['reason'] == 'compiler-artifact' and m['profile']['test'] and m.get('executable'): artifacts.append(m)
|
|
136
|
-
text = ''.join(text)
|
|
137
|
-
found = list(dict.fromkeys(diagnostics(text)))
|
|
138
|
-
if built.returncode: return Results(built.returncode, text, found, [])
|
|
153
|
+
"Build native tests once, with no time limit, then run Cargo's reported executables directly from their package directories. Source edits after building cannot trigger another build; results describe the compiled build, not the latest working tree. `timeout` bounds each binary on its own, excluding compilation. A binary that runs out of time is killed and reported as a failed binary whose suite is `timed_out`. An omitted profile uses tool.fastws.dev-profile, or test. Doctests are not run. This takes no timing lock: rustc's wrapper and tests that time own their holds."
|
|
154
|
+
root, env = Path(root).expanduser().resolve(), {'CARGO_TERM_QUIET': 'false', 'CARGO_TERM_COLOR': 'never'} | (env or {})
|
|
155
|
+
has_profile = any(a in ('-r', '--release', '--profile') or isinstance(a, str) and a.startswith('--profile=') for a in args)
|
|
156
|
+
if not has_profile: args += ('--profile', _dev_profile(root))
|
|
157
|
+
code, text, found, rest = _run(root, ('test', '--no-run', *args), env)
|
|
158
|
+
if code: return Results('test', code, text, found)
|
|
159
|
+
artifacts = [m for m in rest if m['reason'] == 'compiler-artifact' and m['profile']['test'] and m.get('executable')]
|
|
139
160
|
suites, code, start = [], 0, time.monotonic()
|
|
140
161
|
for m in artifacts:
|
|
141
162
|
package = Path(m['manifest_path']).parent
|
|
142
163
|
name = Path(m['target']['src_path']).relative_to(package).as_posix()
|
|
143
164
|
if 'test' not in m['target']['kind']: name = 'unittests ' + name
|
|
144
|
-
|
|
145
|
-
p =
|
|
146
|
-
suites.append(Suite.read(name, p.stdout.splitlines(), p.returncode))
|
|
165
|
+
try: p, late = bounded([m['executable'], *test_args], timeout, merge=True, cwd=package, env=os.environ | env | {'CARGO_MANIFEST_DIR': str(package)}), False
|
|
166
|
+
except TimeoutError as e: p, late = subprocess.CompletedProcess(m['executable'], -signal.SIGKILL, f'{e}\n'), True
|
|
167
|
+
suites.append(Suite.read(name, p.stdout.splitlines(), p.returncode, late))
|
|
147
168
|
text += f' Running {name} ({m["executable"]})\n' + p.stdout
|
|
148
169
|
if p.returncode:
|
|
149
170
|
code = p.returncode
|
|
150
171
|
if '--no-fail-fast' not in args: break
|
|
151
|
-
return Results(code, text, found, suites, time.monotonic() - start)
|
|
172
|
+
return Results('test', code, text, found, suites, time.monotonic() - start)
|
|
152
173
|
|
|
153
174
|
|
|
154
175
|
def sources(root):
|
|
@@ -225,15 +246,15 @@ def replace(source, destination):
|
|
|
225
246
|
return destination
|
|
226
247
|
|
|
227
248
|
|
|
228
|
-
def develop(root):
|
|
229
|
-
"Serialize this project's cooperating develop runs through build, artifact capture, installation, generator and wheel data. Raw Cargo commands do not take this lock."
|
|
249
|
+
def develop(root, *, profile=None):
|
|
250
|
+
"Serialize this project's cooperating develop runs through build, artifact capture, installation, generator and wheel data. An omitted profile uses tool.fastws.dev-profile, or test. Raw Cargo commands do not take this lock."
|
|
230
251
|
root = Path(root).expanduser()
|
|
231
252
|
with store.building(root) as started:
|
|
232
253
|
tool = settings(root)
|
|
233
254
|
cfg = tool['maturin']
|
|
234
255
|
if 'python-source' not in cfg: raise ConfigError(r'cargo develop requires tool.maturin.python-source')
|
|
235
256
|
binaries = tool.get('fastws', {}).get('native-binaries', False)
|
|
236
|
-
artifacts = build(root, binaries=binaries, profile=
|
|
257
|
+
artifacts = build(root, binaries=binaries, profile=profile or _dev_profile(root))
|
|
237
258
|
artifact, = [m for m in artifacts if 'cdylib' in m['target']['crate_types']]
|
|
238
259
|
lib, = [Path(f) for f in artifact['filenames'] if Path(f).suffix in ('.dylib', '.so', '.dll')]
|
|
239
260
|
module = cfg.get('module-name', artifact['target']['name'])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"Fast workspace tools for multi-repo management."
|
|
2
2
|
|
|
3
|
-
__all__ = ["ws_setup", "ws_clone", "ws_pull", "ws_status", "ws_branches", "ws_build", "ws_sync", "ws_add", "ws_remove", "cargo_develop", "cargo_stage", "cargo_wait"]
|
|
3
|
+
__all__ = ["ws_setup", "ws_clone", "ws_pull", "ws_status", "ws_branches", "ws_build", "ws_sync", "ws_add", "ws_remove", "cargo_develop", "cargo_stage", "cargo_wait", "cargo_setup"]
|
|
4
4
|
|
|
5
5
|
import ast, fnmatch, glob, hashlib, json, os, re, shlex, shutil, subprocess, sys, time
|
|
6
6
|
from pathlib import Path
|
|
@@ -14,8 +14,7 @@ from ghapi.core import dep_key, dep_closure
|
|
|
14
14
|
from packaging.requirements import Requirement
|
|
15
15
|
from packaging.utils import canonicalize_name
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
except ModuleNotFoundError: import tomli as tomllib
|
|
17
|
+
from ._toml import tomllib
|
|
19
18
|
|
|
20
19
|
def _repo_lines(repos_file) -> list[str]:
|
|
21
20
|
p = Path(repos_file)
|
|
@@ -571,10 +570,11 @@ def _cli_errors():
|
|
|
571
570
|
def cargo_develop(
|
|
572
571
|
command: str = 'develop', # Subcommand name supplied by Cargo
|
|
573
572
|
path: str = '.', # Rust/Python project directory
|
|
573
|
+
profile: str = None, # Cargo profile; default: tool.fastws.dev-profile, or test
|
|
574
574
|
):
|
|
575
575
|
r"Build and install a mixed Rust/Python project's extension and opted-in binaries."
|
|
576
576
|
from .cargo import develop
|
|
577
|
-
with _cli_errors(): develop(Path(path).resolve())
|
|
577
|
+
with _cli_errors(): develop(Path(path).resolve(), profile=profile)
|
|
578
578
|
|
|
579
579
|
@call_parse(pos=['command'])
|
|
580
580
|
def cargo_stage(
|
|
@@ -603,12 +603,12 @@ def cargo_wait(
|
|
|
603
603
|
patience: int = 1800, # Seconds to wait before giving up
|
|
604
604
|
):
|
|
605
605
|
r"Wait until a Cargo workspace compiles, checking again after each edit to its local packages' sources."
|
|
606
|
-
from .cargo import check,
|
|
606
|
+
from .cargo import check, sources
|
|
607
607
|
root, start = Path(path).resolve(), time.monotonic()
|
|
608
608
|
with _cli_errors(): srcs = sources(root)
|
|
609
609
|
while (res := check(root)).returncode:
|
|
610
|
-
if time.monotonic() - start > patience: raise SystemExit(f'{res.
|
|
611
|
-
print(next((str(d) for d in
|
|
610
|
+
if time.monotonic() - start > patience: raise SystemExit(f'{res.text}gave up after {patience} s')
|
|
611
|
+
print(next((str(d) for d in res.diagnostics if d.level == 'error'), res.text.strip() or 'cargo check failed'), file=sys.stderr)
|
|
612
612
|
before = _newest(srcs)
|
|
613
613
|
while (now := _newest(srcs)) == before and time.monotonic() - start < patience: time.sleep(1)
|
|
614
614
|
while True:
|
|
@@ -721,10 +721,23 @@ def _strip_patch_tables(content: str) -> str:
|
|
|
721
721
|
content = content[:m.start()] + content[end:]
|
|
722
722
|
return content
|
|
723
723
|
|
|
724
|
+
def _patch_text(tables: dict) -> str:
|
|
725
|
+
"TOML `[patch.*]` tables for `tables`, which maps each source URL to its crates' source specs"
|
|
726
|
+
return "\n".join(
|
|
727
|
+
(f"[patch.{url}]" if url == "crates-io" else f'[patch."{url}"]') + "\n"
|
|
728
|
+
+ "".join(f"{name} = {_fmt_source(spec)}\n" for name, spec in sorted(entries.items()))
|
|
729
|
+
for url, entries in tables.items() if entries)
|
|
730
|
+
|
|
724
731
|
def _entry_changes(current, new):
|
|
725
732
|
"Added and removed keys, in the order supplied."
|
|
726
733
|
return [k for k in new if k not in current], [k for k in current if k not in new]
|
|
727
734
|
|
|
735
|
+
def cargo_setup(root):
|
|
736
|
+
"Regenerate local Cargo patches and the compile-lock wrapper without pulling or building."
|
|
737
|
+
root = Path(root).expanduser().resolve()
|
|
738
|
+
_sync_cargo_wrapper(root)
|
|
739
|
+
return _sync_cargo_patches(root)
|
|
740
|
+
|
|
728
741
|
def _sync_cargo_patches(root: Path) -> tuple[list[str], list[str]]:
|
|
729
742
|
"""Regenerate workspace Cargo patches, default to quiet output, and return (added, removed).
|
|
730
743
|
|
|
@@ -756,10 +769,7 @@ def _sync_cargo_patches(root: Path) -> tuple[list[str], list[str]]:
|
|
|
756
769
|
for url, entries in desired.items():
|
|
757
770
|
cur = tables.setdefault(url, {})
|
|
758
771
|
for name, path in entries.items(): cur.setdefault(name, {"path": path})
|
|
759
|
-
body =
|
|
760
|
-
(f"[patch.{url}]" if url == "crates-io" else f'[patch."{url}"]') + "\n"
|
|
761
|
-
+ "".join(f"{name} = {_fmt_source(spec)}\n" for name, spec in sorted(entries.items()))
|
|
762
|
-
for url, entries in tables.items() if entries)
|
|
772
|
+
body = _patch_text(tables)
|
|
763
773
|
new = (_strip_patch_tables(content).rstrip() + "\n\n" + body).lstrip() + "\n" if body else _strip_patch_tables(content)
|
|
764
774
|
if new == original: return [], []
|
|
765
775
|
tomllib.loads(new) # never write an unparseable config
|
|
@@ -991,8 +1001,7 @@ async def ws_sync(
|
|
|
991
1001
|
projects = _build_projects(root, repos_file)
|
|
992
1002
|
_sync_build_isolation(pyproject_path, projects)
|
|
993
1003
|
|
|
994
|
-
|
|
995
|
-
added_p, removed_p = _sync_cargo_patches(root)
|
|
1004
|
+
added_p, removed_p = cargo_setup(root)
|
|
996
1005
|
if added_p: print(f"Cargo patches added: {', '.join(added_p)}")
|
|
997
1006
|
if removed_p: print(f"Cargo patches removed: {', '.join(removed_p)}")
|
|
998
1007
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
r"""Build several repos' working trees together as one snapshot, apart from the live trees, and group its compiler messages by owner.
|
|
2
|
+
|
|
3
|
+
Each run snapshots every repo's working tree as a commit, checks the commits out as worktrees under one root, and gives that root the workspace's Cargo config with the root's own crates patched in. Builds there never see a file halfway through an edit, and never touch the files that people and agents are editing."""
|
|
4
|
+
import fnmatch, os, posixpath, re, subprocess, tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from ._toml import tomllib
|
|
7
|
+
from .core import _local_crates, _patch_text, _strip_patch_tables, _sync_cargo_patches
|
|
8
|
+
from .cargo import check
|
|
9
|
+
|
|
10
|
+
__all__ = ["snapshot", "integrate", "by_owner", "owners", "errors", "code_count", "code_lines", "record"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _git(repo, *args, env=None):
|
|
14
|
+
return subprocess.run(["git", *args], cwd=repo, env=env, stdout=subprocess.PIPE, text=True, check=True).stdout.strip()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def snapshot(repo):
|
|
18
|
+
"A commit of `repo`'s working tree on top of `HEAD`, with untracked files and without ignored ones. `HEAD` and the index don't change."
|
|
19
|
+
repo = Path(repo).expanduser()
|
|
20
|
+
with tempfile.TemporaryDirectory() as d:
|
|
21
|
+
env = {**os.environ, "GIT_INDEX_FILE": str(Path(d)/"index")}
|
|
22
|
+
_git(repo, "read-tree", "HEAD", env=env)
|
|
23
|
+
_git(repo, "add", "-A", env=env)
|
|
24
|
+
tree = _git(repo, "write-tree", env=env)
|
|
25
|
+
return _git(repo, "commit-tree", tree, "-p", "HEAD", "-m", "integration snapshot")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _checkout(repo, dest, commit):
|
|
29
|
+
"Check `commit` out at `dest`, a detached worktree of `repo` that this adds if it's missing, removing the last run's untracked files."
|
|
30
|
+
if not dest.exists(): return _git(repo, "worktree", "add", "--detach", str(dest), commit)
|
|
31
|
+
_git(dest, "checkout", "--detach", "-f", commit)
|
|
32
|
+
_git(dest, "clean", "-fd")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _config(root, workspace):
|
|
36
|
+
"Write `root`'s Cargo config: `workspace`'s, with the patches for crates under `root` pointing there."
|
|
37
|
+
held = {pkg["name"] for pkg, _ in _local_crates(root)}
|
|
38
|
+
content = (workspace/".cargo"/"config.toml").read_text()
|
|
39
|
+
kept = {url: {n: s for n, s in entries.items() if n not in held} for url, entries in tomllib.loads(content).get("patch", {}).items()}
|
|
40
|
+
config = root/".cargo"/"config.toml"
|
|
41
|
+
config.parent.mkdir(exist_ok=True)
|
|
42
|
+
config.write_text(_strip_patch_tables(content).rstrip() + "\n\n" + _patch_text(kept) + "\n")
|
|
43
|
+
_sync_cargo_patches(root)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def integrate(
|
|
47
|
+
repos:dict, # Each repo's name and its live checkout; the snapshot goes in `root/<name>`
|
|
48
|
+
root, # Directory holding the snapshot worktrees and their Cargo config
|
|
49
|
+
workspace="~/aai-ws", # Workspace whose Cargo config the root copies
|
|
50
|
+
)->dict:
|
|
51
|
+
"Snapshot each of `repos`, check the snapshots out under `root`, and write `root`'s Cargo config. Returns each repo's snapshot commit."
|
|
52
|
+
root = Path(root).expanduser().resolve()
|
|
53
|
+
commits = {}
|
|
54
|
+
for name, repo in repos.items():
|
|
55
|
+
repo = Path(repo).expanduser()
|
|
56
|
+
commits[name] = snapshot(repo)
|
|
57
|
+
_checkout(repo, root/name, commits[name])
|
|
58
|
+
_config(root, Path(workspace).expanduser().resolve())
|
|
59
|
+
return commits
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def by_owner(
|
|
63
|
+
diagnostics:dict, # Each repo's name and its `fastws.cargo.Diagnostic`s, with paths relative to the repo, such as one check's errors
|
|
64
|
+
owners:dict, # Each owner's path globs, matched against `<repo>/<path>`
|
|
65
|
+
)->dict:
|
|
66
|
+
"Group `diagnostics` by owner, then by file, with each path prefixed by its repo's name. A path that no owner's globs match groups under `None`."
|
|
67
|
+
groups = {}
|
|
68
|
+
for repo, ds in diagnostics.items():
|
|
69
|
+
for d in ds:
|
|
70
|
+
d = d._replace(path=posixpath.normpath(f"{repo}/{d.path}"))
|
|
71
|
+
owner = next((o for o, globs in owners.items() if any(fnmatch.fnmatch(d.path, g) for g in globs)), None)
|
|
72
|
+
groups.setdefault(owner, {}).setdefault(d.path, []).append(d)
|
|
73
|
+
return groups
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def owners(
|
|
77
|
+
path, # Markdown file with a `Files` heading (expands `~`)
|
|
78
|
+
)->dict:
|
|
79
|
+
"Each owner's path globs from the bullets under `path`'s `Files` heading, one `- Owner: `glob` `glob`` line each, in order, as `by_owner` takes them. The first owner whose globs match a path owns it."
|
|
80
|
+
files = re.split(r"^#+ Files\s*$", Path(path).expanduser().read_text(), maxsplit=1, flags=re.M)[1]
|
|
81
|
+
files = re.split(r"^#", files, maxsplit=1, flags=re.M)[0]
|
|
82
|
+
return {m[1]: re.findall(r"`([^`]+)`", m[2]) for m in re.finditer(r"^- ([^:]+):(.*)$", files, re.M)}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def errors(
|
|
86
|
+
root, # Directory holding the snapshot worktrees (expands `~`)
|
|
87
|
+
repos, # Names of the worktrees under `root` to check, in order
|
|
88
|
+
)->dict:
|
|
89
|
+
"Each repo's compiler errors in its own files, from checking its worktree under `root`, as `by_owner` takes them. A check repeats a dependency's errors under a path outside the repo; the dependency's own check reports those."
|
|
90
|
+
root = Path(root).expanduser()
|
|
91
|
+
return {r: [d for d in check(root/r).diagnostics if d.level == "error" and not d.path.startswith(("/", ".."))] for r in repos}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def code_count(
|
|
95
|
+
lines, # Rust source lines in order, such as one file's or one side of a diff hunk
|
|
96
|
+
)->int:
|
|
97
|
+
"How many of `lines` are code: neither blank nor comments (`//` lines and `/* */` blocks)."
|
|
98
|
+
n, block = 0, False
|
|
99
|
+
for line in lines:
|
|
100
|
+
s = line.strip()
|
|
101
|
+
if block: block = "*/" not in s
|
|
102
|
+
elif s.startswith("/*"): block = "*/" not in s
|
|
103
|
+
elif s and not s.startswith("//"): n += 1
|
|
104
|
+
return n
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def code_lines(
|
|
108
|
+
path, # A directory, such as a crate's `src/` (expands `~`)
|
|
109
|
+
)->int:
|
|
110
|
+
"The code lines of the Rust files under `path`, as `code_count` counts them."
|
|
111
|
+
return sum(code_count(f.read_text().splitlines()) for f in Path(path).expanduser().rglob("*.rs"))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def record(
|
|
115
|
+
path, # Markdown file to write
|
|
116
|
+
title:str, # The record's heading, such as "Run 8"
|
|
117
|
+
commits:dict, # Each repo's snapshot commit, as `integrate` returns them
|
|
118
|
+
groups:dict, # Messages by owner and file, as `by_owner` returns them
|
|
119
|
+
edits=(), # Edits made in the snapshot only, one line each
|
|
120
|
+
tests:dict=None, # Each repo's name and its test `Results`, as `fastws.cargo.run_tests` returns them
|
|
121
|
+
sizes:dict=None, # Code lines by label, as `code_lines` counts them, such as each repo's `src/` beside its baselines
|
|
122
|
+
sections:dict=None, # Further sections by heading, each text written as a code block, such as each repo's pytest output
|
|
123
|
+
)->dict:
|
|
124
|
+
"Write a run's record: the snapshots, each owner's messages by file as `line: message`, any snapshot-only edits, each test binary's outcome with its failures' output, code-line sizes, and any further `sections`. Returns each owner's message count."
|
|
125
|
+
counts = {o: sum(map(len, files.values())) for o, files in groups.items()}
|
|
126
|
+
lines = [f"# {title}", "", "Snapshots: " + ", ".join(f"{name} `{c[:9]}`" for name, c in commits.items()), ""]
|
|
127
|
+
for owner, files in groups.items():
|
|
128
|
+
lines += [f"## {owner or 'no owner'}: {counts[owner]}", ""]
|
|
129
|
+
for file, ds in files.items(): lines += [f"- `{file}`", *[f" - {d.line}: {d.message}" for d in ds]]
|
|
130
|
+
lines.append("")
|
|
131
|
+
if edits: lines += ["## Snapshot-only edits", "", *[f"- {e}" for e in edits], ""]
|
|
132
|
+
if tests: lines += ["## Tests", "", *[f"### {name}\n\n```\n{r.summary(None)}\n```\n" for name, r in tests.items()]]
|
|
133
|
+
if sizes: lines += ["## Size (code lines, no comments or blanks)", "", *[f"- {k}: {v:,}" for k, v in sizes.items()], ""]
|
|
134
|
+
for heading, text in (sections or {}).items(): lines += [f"## {heading}", "", f"```\n{text}\n```", ""]
|
|
135
|
+
Path(path).expanduser().write_text("\n".join(lines))
|
|
136
|
+
return counts
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
r"""Builds that `cargo develop` installs, each kept once, read-only, and named by the hash of its inputs and files. A project's installed build is a pointer to one of them, which installing replaces in one step, so a process or a timing that resolves a build never sees it change.
|
|
2
2
|
|
|
3
3
|
A build is a directory under `STORE/<project>/`, named by its id, that holds `build.json` and a hard link to a blob for each file: `python/…` for the Python package with its extension, `bin/…` for binaries and `data/…` for wheel data. Each blob, under `STORE/blobs/`, is named by its contents' sha256, so files that don't change between builds take no more space. Installed paths are symbolic links through `STORE/<project>/installed`, a link to the installed build's directory. Windows installs copies."""
|
|
4
|
-
import hashlib, json, os, re, shutil, time
|
|
4
|
+
import hashlib, json, os, re, shutil, time
|
|
5
5
|
if os.name != 'nt': import fcntl
|
|
6
6
|
from contextlib import contextmanager
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from tempfile import TemporaryDirectory
|
|
10
|
+
from ._toml import tomllib
|
|
10
11
|
|
|
11
12
|
STORE = Path(os.environ.get('XDG_CACHE_HOME') or '~/.cache').expanduser()/'fastws'/'builds' # Each project's builds, and the blobs that their files link to
|
|
12
13
|
KEEP, KEEP_DAYS = 20, 2 # `collect` keeps each project's installed build, its last `KEEP` builds and its builds of the last `KEEP_DAYS` days
|
|
@@ -32,7 +33,10 @@ def access(exclusive=False):
|
|
|
32
33
|
yield
|
|
33
34
|
|
|
34
35
|
def _digest(path):
|
|
35
|
-
|
|
36
|
+
digest = hashlib.sha256()
|
|
37
|
+
with open(path, 'rb') as f:
|
|
38
|
+
while chunk := f.read(1024*1024): digest.update(chunk)
|
|
39
|
+
return digest.hexdigest()
|
|
36
40
|
|
|
37
41
|
def _blob(path):
|
|
38
42
|
"The blob that holds `path`'s contents, read-only and executable where `path` is, added to the store where it's new."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastws-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.34
|
|
4
4
|
Summary: Fast workspace tools for multi-repo management
|
|
5
5
|
Author: Jeremy Howard
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -205,9 +205,10 @@ Build a mixed Rust/Python project's extension and opted-in binaries, keep them w
|
|
|
205
205
|
```bash
|
|
206
206
|
cargo develop
|
|
207
207
|
cargo develop --path /path/to/project
|
|
208
|
+
cargo develop --profile quick
|
|
208
209
|
```
|
|
209
210
|
|
|
210
|
-
Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`.
|
|
211
|
+
Run from the project root, or give its directory with `--path`. The command reads `python-source`, `module-name`, `manifest-path` and features from `[tool.maturin]`. `[tool.fastws] dev-profile = "quick"` selects a project's default for both `cargo develop` and `fastws.cargo.run_tests`. Without that setting they use `test`. An explicit `--profile NAME` overrides the project default. The project must produce a `cdylib` and set `python-source`. Define the selected profile in `Cargo.toml` and keep incremental compilation enabled for the local crate.
|
|
211
212
|
|
|
212
213
|
Make the initial editable installation through `ws-sync` or maturin. Repeat that installation when package metadata or entry points change. Running Python processes keep the extension they loaded.
|
|
213
214
|
|
|
@@ -253,11 +254,11 @@ cargo wait --path /path/to/workspace
|
|
|
253
254
|
|
|
254
255
|
Use it when another process's unfinished edits stop the workspace compiling, such as a second agent working in the same tree. Each check runs `cargo check --all-targets`, whose rustc calls hold the timing lock shared, as every build's do. After a failed check, `cargo wait` prints Cargo's first error line. It checks again once a source file of a local package changes and then stays unchanged for 2 seconds. Local packages are the workspace's members and its path dependencies, such as a sibling crate that `ws-sync` patches in. When a check passes, it prints `compiles` and exits. After `--patience` seconds, 1800 by default, it prints the last check's errors and exits with status 1.
|
|
255
256
|
|
|
256
|
-
### Rust tests from Python
|
|
257
|
+
### Rust builds and tests from Python
|
|
257
258
|
|
|
258
|
-
`fastws.cargo.run_tests` builds native tests once with Cargo, without a time limit, then runs the executables that Cargo reports directly from their package directories.
|
|
259
|
+
`fastws.cargo.run_tests` builds native tests once with Cargo, without a time limit, then runs the executables that Cargo reports directly from their package directories. Each binary gets its own `timeout`, 30 seconds by default, excluding compilation. A binary still running when its `timeout` ends is killed and counts as a failed binary, with its suite's `timed_out` set. Source edits between building and running do not trigger another build: the results describe the compiled build, not necessarily the latest working tree. It supports directly runnable native tests, not cross-target runners.
|
|
259
260
|
|
|
260
|
-
Pass Cargo build/selection options positionally, and test filters and harness options in `test_args`. A failed binary stops the run unless `--no-fail-fast` is given. Results contain compiler diagnostics and each binary's outcomes and captured failures.
|
|
261
|
+
The default profile is `[tool.fastws].dev-profile`, or `test` when unset. Explicit `--profile NAME`, `--profile=NAME`, `--release` and `-r` override it. Pass Cargo build/selection options positionally, and test filters and harness options in `test_args`. A failed binary stops the run unless `--no-fail-fast` is given. Results contain compiler diagnostics and each binary's outcomes and captured failures.
|
|
261
262
|
|
|
262
263
|
```python
|
|
263
264
|
from fastws.cargo import run_tests
|
|
@@ -268,6 +269,34 @@ focused = run_tests('.', '--test', 'core', test_args=('test_name', '--nocapture'
|
|
|
268
269
|
|
|
269
270
|
Doctests are deliberately not run; keep executable examples in ordinary Rust tests. `run_tests` accepts `env` overrides and `timeout=None` to disable its watchdog. It takes no timing lock around the run: rustc's wrapper owns its shared holds, and tests that time own their exclusive holds.
|
|
270
271
|
|
|
272
|
+
`fastws.cargo.check` runs any Cargo subcommand, `check --all-targets` by default, and returns the same `Results` without test suites. A failing run is a result, not an exception. Each `Diagnostic` in its `diagnostics` holds the message's path, line, column, level and text, and its error code or lint as `code`, such as `E0308` or `clippy::too_many_lines`. The summary lists errors first. `env` sets environment variables for the run, as for `run_tests`. A subcommand that rejects `--message-format`, such as `cargo llvm-lines`, runs with `diagnostics=False`: its result holds the exit status and output, with no diagnostics.
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from fastws.cargo import check
|
|
276
|
+
|
|
277
|
+
lints = check('.', 'clippy', '--all-targets', '--', '-W', 'clippy::too_many_lines', env={'CLIPPY_CONF_DIR': 'review'})
|
|
278
|
+
errors = [d for d in lints.diagnostics if d.level == 'error']
|
|
279
|
+
sizes = check('.', 'llvm-lines', '--lib', diagnostics=False).text
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Integration snapshots
|
|
283
|
+
|
|
284
|
+
`fastws.integrate` builds several repos together while people or agents keep editing them. `integrate` snapshots each repo's working tree as a commit (untracked files included, ignored files not), checks each commit out as a detached worktree under one root, and writes the root's Cargo config. `HEAD` and the index of each live repo don't change. The config is the workspace's, with the patches for crates under the root pointing at the root's worktrees, so a build there reads the snapshots and every other local crate from the workspace. Keep the root outside the workspace, where `ws-sync` and uv don't treat its worktrees as members.
|
|
285
|
+
|
|
286
|
+
`errors` checks each snapshot in turn and keeps the errors in that repo's own files. A repo's check also reports its dependencies' errors, under paths outside the repo, and each dependency's own check already covers those. `owners` reads each owner's path globs from the bullets under a Markdown file's `Files` heading, one ``- Owner: `glob` `glob` `` line each, in order. `by_owner` groups each repo's compiler messages by owner, then by file, and the first owner whose globs match a path owns it. It prefixes each path with its repo's name, so a path into a sibling repo, such as `../rustymath/src/x.rs` from basedpl's check, groups as `rustymath/src/x.rs`. `run_tests` with `--no-fail-fast` runs every test binary, each within its own `timeout`, so a slow or crashing binary doesn't stop the others. `code_count` counts the Rust code lines in a sequence of lines, leaving out blanks and comments. `code_lines` applies it to each Rust file under a directory. `exhash`'s `Journal.stat(count=code_count)` counts the code lines that edits add and remove. `record` writes a run's messages, each repo's test `Results` with each failure's output, and any sizes, as a Markdown record, and returns each owner's message count.
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from fastws.cargo import run_tests
|
|
290
|
+
from fastws.integrate import by_owner, code_lines, errors, integrate, owners, record
|
|
291
|
+
|
|
292
|
+
root = '~/git/integration'
|
|
293
|
+
commits = integrate({'basedpl': '~/aai-ws/basedpl', 'rustymath': '~/aai-ws/rustymath'}, root)
|
|
294
|
+
errs = errors(root, ['rustymath', 'basedpl'])
|
|
295
|
+
groups = by_owner(errs, owners('~/aai-ws/basedpl/meta/owners.md'))
|
|
296
|
+
tests = {r: run_tests(f'{root}/{r}', '--no-fail-fast') for r in errs if not errs[r]}
|
|
297
|
+
sizes = {r: code_lines(f'{root}/{r}/src') for r in errs}
|
|
298
|
+
counts = record(f'{root}/run8.md', 'Run 8', commits, groups, tests=tests, sizes=sizes)
|
|
299
|
+
```
|
|
271
300
|
### `ws-sync`
|
|
272
301
|
|
|
273
302
|
Syncing proceeds in this order:
|
|
@@ -293,6 +322,8 @@ Do not commit a `Cargo.lock` generated under these patches. Its source-less loca
|
|
|
293
322
|
|
|
294
323
|
Sync writes `.cargo/rustc-lock` and makes it Cargo's `rustc-wrapper`. The script runs each rustc call holding the timing lock shared (see `cargo develop` above). Inside the lock it runs the wrapper that the config named before sync first wrote the script, where another tool set one, and otherwise `sccache` when it's installed, so unchanged compilation units are reused across the workspace's otherwise-independent Cargo target directories. Later syncs keep that inner wrapper. The script costs about 9 ms a rustc call, mostly the Python interpreter's start. If `sccache` isn't installed, sync warns with the install command (`cargo install sccache --locked`) and continues; rerun `ws-sync` after installing to enable it. On Windows, where the lock does nothing, the config keeps another tool's wrapper, or else names `sccache`.
|
|
295
324
|
|
|
325
|
+
For an isolated build workspace, `fastws.core.cargo_setup(root)` regenerates these Cargo patches and the same compiler wrapper without pulling, syncing Python or building.
|
|
326
|
+
|
|
296
327
|
Fastws discovers JavaScript packages directly under the workspace root. It reads each package's `workspaces` list for declared nested packages. These lists accept paths and globs. Fastws ignores undeclared nested packages. Lockfiles do not affect discovery. `[tool.fastws].exclude` matches paths relative to the workspace root. Fastws skips `node_modules` and directories whose names start with `.` or `_`.
|
|
297
328
|
|
|
298
329
|
Each sync updates `workspaces` in the root `package.json`. It creates the file when needed. It preserves external paths, globs, and unrelated settings. It reports added and removed packages.
|
|
@@ -2,9 +2,11 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
fastws/__init__.py
|
|
5
|
+
fastws/_toml.py
|
|
5
6
|
fastws/build_backend.py
|
|
6
7
|
fastws/cargo.py
|
|
7
8
|
fastws/core.py
|
|
9
|
+
fastws/integrate.py
|
|
8
10
|
fastws/news.py
|
|
9
11
|
fastws/procs.py
|
|
10
12
|
fastws/releases.py
|
|
@@ -18,6 +20,7 @@ fastws_cli.egg-info/requires.txt
|
|
|
18
20
|
fastws_cli.egg-info/top_level.txt
|
|
19
21
|
tests/test_build.py
|
|
20
22
|
tests/test_build_backend.py
|
|
23
|
+
tests/test_integrate.py
|
|
21
24
|
tests/test_news.py
|
|
22
25
|
tests/test_procs.py
|
|
23
26
|
tests/test_releases.py
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import subprocess, pytest, fastws.core as core
|
|
2
|
-
from fastws.cargo import build, run_tests,
|
|
2
|
+
from fastws.cargo import build, check, run_tests, Suite
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
def _mk_proj(root, name, extra=''):
|
|
@@ -41,7 +41,9 @@ def test_build_diagnostics(tmp_path, capfd, body, diagnostic):
|
|
|
41
41
|
else: assert [m['target']['name'] for m in build(tmp_path)] == ['app']
|
|
42
42
|
stderr = capfd.readouterr().err
|
|
43
43
|
if diagnostic: assert f'[dep] dep/src/lib.rs:1:' in stderr and f': {diagnostic}:' in stderr
|
|
44
|
-
else: assert not
|
|
44
|
+
else: assert 'lib.rs:' not in stderr
|
|
45
|
+
checked = check(tmp_path)
|
|
46
|
+
assert checked.ok == (diagnostic != 'error') and [(d.path, d.level) for d in checked.diagnostics] == ([('dep/src/lib.rs', diagnostic)] if diagnostic else [])
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
def test_run_tests(tmp_path):
|
|
@@ -64,12 +66,31 @@ pub fn two() -> i32 { 2 }
|
|
|
64
66
|
(tmp_path/'.cargo/config.toml').write_text('[term]\nquiet = true\n')
|
|
65
67
|
r = run_tests(tmp_path, '--no-fail-fast')
|
|
66
68
|
assert not r.ok
|
|
67
|
-
assert [(d.path, d.line, d.level) for d in r.diagnostics] == [('src/lib.rs', 1, 'warning')]
|
|
69
|
+
assert [(d.path, d.line, d.level, d.code) for d in r.diagnostics] == [('src/lib.rs', 1, 'warning', 'unused_variables')]
|
|
68
70
|
suites = {s.name: s for s in r.suites}
|
|
69
71
|
unit, more = suites['unittests src/lib.rs'], suites['tests/more.rs']
|
|
70
72
|
assert len(suites) == 2
|
|
71
73
|
assert unit.tests == {'t::passes': 'ok', 't::fails': 'FAILED', 't::skipped': 'ignored'} and unit.failed
|
|
72
74
|
assert 'broken' in unit.failures['t::fails'] and list(unit.failures) == ['t::fails']
|
|
73
75
|
assert more.tests == {'other': 'ok'} and not more.failed
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
slow = run_tests(tmp_path, '--lib', test_args=('skipped', '--ignored', '--nocapture'), timeout=.5)
|
|
77
|
+
s, = slow.suites
|
|
78
|
+
assert not slow.ok and s.failed and s.timed_out and 'started slow test' in s.output
|
|
79
|
+
assert str(s) == 'unittests src/lib.rs: ran out of time'
|
|
80
|
+
|
|
81
|
+
(tmp_path/'pyproject.toml').write_text(r'tool.fastws.dev-profile = "missing"')
|
|
82
|
+
r = run_tests(tmp_path, '--test', 'more')
|
|
83
|
+
assert not r.ok and not r.suites and 'missing' in r.text
|
|
84
|
+
for args in [('--profile', 'test'), ('--profile=test',), ('--release',), ('-r',)]:
|
|
85
|
+
assert run_tests(tmp_path, '--test', 'more', *args).ok
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_captured_test_output():
|
|
89
|
+
for failed in (False, True):
|
|
90
|
+
lines = ['test passes ... ok', 'successes:', '---- passes stdout ----', 'printed', 'successes:', 'passes']
|
|
91
|
+
if failed:
|
|
92
|
+
lines[1:1] = ['test fails ... FAILED']
|
|
93
|
+
lines += ['failures:', '---- fails stdout ----', 'broken', 'failures:', 'fails']
|
|
94
|
+
lines.append(f'test result: {"FAILED" if failed else "ok"}. 1 passed; {int(failed)} failed; 0 ignored')
|
|
95
|
+
s = Suite.read('unit', lines, int(failed))
|
|
96
|
+
assert s.failures == ({'fails': 'broken'} if failed else {}) and 'printed' in s.output
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from fastws._toml import tomllib
|
|
3
|
+
from fastws.integrate import _config, code_lines, owners, snapshot
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def git(repo, *args): return subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True, text=True).stdout
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_snapshot_takes_the_working_tree_and_leaves_head_and_index(tmp_path):
|
|
10
|
+
git(tmp_path, "init", "-q")
|
|
11
|
+
(tmp_path/".gitignore").write_text("ignored\n")
|
|
12
|
+
(tmp_path/"kept").write_text("old")
|
|
13
|
+
git(tmp_path, "add", "-A"); git(tmp_path, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "base")
|
|
14
|
+
head = git(tmp_path, "rev-parse", "HEAD")
|
|
15
|
+
(tmp_path/"kept").write_text("new"); (tmp_path/"untracked").write_text("u"); (tmp_path/"ignored").write_text("i")
|
|
16
|
+
commit = snapshot(tmp_path)
|
|
17
|
+
assert git(tmp_path, "show", f"{commit}:kept") == "new"
|
|
18
|
+
assert git(tmp_path, "show", f"{commit}:untracked") == "u"
|
|
19
|
+
assert "ignored" not in git(tmp_path, "ls-tree", "--name-only", commit).split()
|
|
20
|
+
assert git(tmp_path, "rev-parse", "HEAD") == head and git(tmp_path, "diff", "--cached", "--name-only") == ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_config_points_only_the_roots_crates_at_the_root(tmp_path):
|
|
24
|
+
workspace, root = tmp_path/"ws", tmp_path/"root"
|
|
25
|
+
(workspace/".cargo").mkdir(parents=True)
|
|
26
|
+
(workspace/".cargo"/"config.toml").write_text(
|
|
27
|
+
f'[build]\nrustc-wrapper = "/lock"\n\n[patch.crates-io]\na = {{ path = "{workspace}/a" }}\nb = {{ path = "{workspace}/b" }}\n')
|
|
28
|
+
(root/"a").mkdir(parents=True)
|
|
29
|
+
(root/"a"/"Cargo.toml").write_text('[package]\nname = "a"\nversion = "0.1.0"\n')
|
|
30
|
+
_config(root, workspace)
|
|
31
|
+
config = tomllib.loads((root/".cargo"/"config.toml").read_text())
|
|
32
|
+
assert config["build"]["rustc-wrapper"] == "/lock"
|
|
33
|
+
assert config["patch"]["crates-io"] == {"a": {"path": str(root/"a")}, "b": {"path": f"{workspace}/b"}}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_code_lines_skips_blanks_and_comments(tmp_path):
|
|
37
|
+
(tmp_path/"a").mkdir()
|
|
38
|
+
(tmp_path/"a"/"m.rs").write_text("//! Module doc\n\nfn f() {}\n/* a\n block */\nlet s = \"// not a comment\";\n /// doc\n}\n")
|
|
39
|
+
(tmp_path/"notes.md").write_text("fn f() {}\n")
|
|
40
|
+
assert code_lines(tmp_path) == 3
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_owners_reads_the_files_section_in_order(tmp_path):
|
|
44
|
+
(tmp_path/"owners.md").write_text(
|
|
45
|
+
"# Owners\n\n- Before: `x/*`\n\n## Files\n\nThe first match owns a path.\n- Structure: `a/src/p.rs` `b/src/s.rs`\n- Numeric: `b/*`\n\n## Order\n\n- After: `c/*`\n")
|
|
46
|
+
assert list(owners(tmp_path/"owners.md").items()) == [("Structure", ["a/src/p.rs", "b/src/s.rs"]), ("Numeric", ["b/*"])]
|
|
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
|