fastws-cli 0.0.34__tar.gz → 0.0.35__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.34 → fastws_cli-0.0.35}/PKG-INFO +8 -3
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/README.md +7 -2
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws/__init__.py +2 -1
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws/cargo.py +108 -23
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws/core.py +32 -15
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws_cli.egg-info/PKG-INFO +8 -3
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/tests/test_build.py +50 -9
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/tests/test_integrate.py +2 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/tests/test_procs.py +11 -4
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/tests/test_sync.py +11 -8
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/LICENSE +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws/_toml.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws/build_backend.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws/integrate.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws/news.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws/procs.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws/releases.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws/store.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws/timelock.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws_cli.egg-info/SOURCES.txt +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws_cli.egg-info/dependency_links.txt +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws_cli.egg-info/entry_points.txt +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws_cli.egg-info/requires.txt +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/fastws_cli.egg-info/top_level.txt +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/pyproject.toml +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/setup.cfg +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/tests/test_build_backend.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/tests/test_news.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/tests/test_releases.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/tests/test_setup.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/tests/test_store.py +0 -0
- {fastws_cli-0.0.34 → fastws_cli-0.0.35}/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.35
|
|
4
4
|
Summary: Fast workspace tools for multi-repo management
|
|
5
5
|
Author: Jeremy Howard
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -256,7 +256,11 @@ Use it when another process's unfinished edits stop the workspace compiling, suc
|
|
|
256
256
|
|
|
257
257
|
### Rust builds and tests from Python
|
|
258
258
|
|
|
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.
|
|
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. Outside `FASTWS_JOB`, a timed-out binary is reported as a failed suite with `timed_out` set. Inside a marked job, its timeout raises so the job owner can clean up descendants. 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.
|
|
260
|
+
|
|
261
|
+
Where a root has `.config/nextest.toml`, `cargo nextest run` runs the built tests instead: each test in its own process, every binary at once, with the config's slow-test timeouts and groups. `profiles` lists the nextest profiles to run one after another, `('default',)` unless given. `timeout` bounds each profile's run, and each binary's results come from nextest's JUnit report, which lists no ignored tests. `--profile NAME` passes to nextest as `--cargo-profile NAME`, and `test_args` follow `--`.
|
|
262
|
+
|
|
263
|
+
`root` can also be a list of roots, which build one after another. Roots without nextest then run their binaries in turn. Each profile then runs at the same time in every root whose config defines it: tests in different roots share a phase, and no phase starts before the previous one ends. Suite names start with the root's directory name, and with several profiles also with the profile, as in `app timed: tests/speed.rs`. A failure ends the run after its profile unless `--no-fail-fast` is given.
|
|
260
264
|
|
|
261
265
|
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.
|
|
262
266
|
|
|
@@ -265,6 +269,7 @@ from fastws.cargo import run_tests
|
|
|
265
269
|
|
|
266
270
|
native = run_tests('.', '--no-fail-fast')
|
|
267
271
|
focused = run_tests('.', '--test', 'core', test_args=('test_name', '--nocapture'))
|
|
272
|
+
phased = run_tests(['app', 'lib'], profiles=('untimed', 'timed'))
|
|
268
273
|
```
|
|
269
274
|
|
|
270
275
|
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.
|
|
@@ -314,7 +319,7 @@ Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level dir
|
|
|
314
319
|
|
|
315
320
|
Existing globs, entries for missing directories, and entries for checkouts that are still not Python projects are retained. Use `exclude = [...]` under `[tool.fastws]` to specify exclusions the scan cannot infer, such as keeping a valid project out of the workspace. Adding members preserves hand-written `[tool.uv.sources]` entries, including path and Git sources.
|
|
316
321
|
|
|
317
|
-
Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `[patch.crates-io]` gets an entry for each local crate, including nested Cargo workspace members,
|
|
322
|
+
Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `[patch.crates-io]` gets an entry for each local crate, including nested Cargo workspace members, that some local manifest requires from crates.io: by version alone, without `path`, `git` or `workspace`, in any dependency table or `[workspace.dependencies]`. Git dependencies that name local crates get entries under the matching URL. Builds under the workspace root use these local checkouts. Entries pointing outside the root and other configuration sections are preserved.
|
|
318
323
|
|
|
319
324
|
Sync also defaults `[term] quiet = true` in that file to suppress routine Cargo output and warnings. An explicit `quiet = false` is preserved.
|
|
320
325
|
|
|
@@ -228,7 +228,11 @@ Use it when another process's unfinished edits stop the workspace compiling, suc
|
|
|
228
228
|
|
|
229
229
|
### Rust builds and tests from Python
|
|
230
230
|
|
|
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.
|
|
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. Outside `FASTWS_JOB`, a timed-out binary is reported as a failed suite with `timed_out` set. Inside a marked job, its timeout raises so the job owner can clean up descendants. 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.
|
|
232
|
+
|
|
233
|
+
Where a root has `.config/nextest.toml`, `cargo nextest run` runs the built tests instead: each test in its own process, every binary at once, with the config's slow-test timeouts and groups. `profiles` lists the nextest profiles to run one after another, `('default',)` unless given. `timeout` bounds each profile's run, and each binary's results come from nextest's JUnit report, which lists no ignored tests. `--profile NAME` passes to nextest as `--cargo-profile NAME`, and `test_args` follow `--`.
|
|
234
|
+
|
|
235
|
+
`root` can also be a list of roots, which build one after another. Roots without nextest then run their binaries in turn. Each profile then runs at the same time in every root whose config defines it: tests in different roots share a phase, and no phase starts before the previous one ends. Suite names start with the root's directory name, and with several profiles also with the profile, as in `app timed: tests/speed.rs`. A failure ends the run after its profile unless `--no-fail-fast` is given.
|
|
232
236
|
|
|
233
237
|
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.
|
|
234
238
|
|
|
@@ -237,6 +241,7 @@ from fastws.cargo import run_tests
|
|
|
237
241
|
|
|
238
242
|
native = run_tests('.', '--no-fail-fast')
|
|
239
243
|
focused = run_tests('.', '--test', 'core', test_args=('test_name', '--nocapture'))
|
|
244
|
+
phased = run_tests(['app', 'lib'], profiles=('untimed', 'timed'))
|
|
240
245
|
```
|
|
241
246
|
|
|
242
247
|
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.
|
|
@@ -286,7 +291,7 @@ Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level dir
|
|
|
286
291
|
|
|
287
292
|
Existing globs, entries for missing directories, and entries for checkouts that are still not Python projects are retained. Use `exclude = [...]` under `[tool.fastws]` to specify exclusions the scan cannot infer, such as keeping a valid project out of the workspace. Adding members preserves hand-written `[tool.uv.sources]` entries, including path and Git sources.
|
|
288
293
|
|
|
289
|
-
Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `[patch.crates-io]` gets an entry for each local crate, including nested Cargo workspace members,
|
|
294
|
+
Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `[patch.crates-io]` gets an entry for each local crate, including nested Cargo workspace members, that some local manifest requires from crates.io: by version alone, without `path`, `git` or `workspace`, in any dependency table or `[workspace.dependencies]`. Git dependencies that name local crates get entries under the matching URL. Builds under the workspace root use these local checkouts. Entries pointing outside the root and other configuration sections are preserved.
|
|
290
295
|
|
|
291
296
|
Sync also defaults `[term] quiet = true` in that file to suppress routine Cargo output and warnings. An explicit `quiet = false` is preserved.
|
|
292
297
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
r"Build, test and install Cargo artifacts for mixed Rust/Python projects."
|
|
2
|
-
import json, os, re, shutil, signal, subprocess, sys, sysconfig, time
|
|
2
|
+
import json, os, re, shutil, signal, subprocess, sys, sysconfig, tempfile, time
|
|
3
3
|
from collections import Counter
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
5
|
from dataclasses import dataclass, field
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import NamedTuple
|
|
8
|
+
from xml.etree import ElementTree
|
|
7
9
|
from . import store
|
|
8
|
-
from .procs import bounded
|
|
10
|
+
from .procs import bounded, JOB_ENV
|
|
9
11
|
from ._toml import tomllib
|
|
10
12
|
|
|
11
13
|
|
|
@@ -143,32 +145,115 @@ def check(
|
|
|
143
145
|
return Results(args[0], *_run(Path(root).expanduser(), args, env, diagnostics)[:3])
|
|
144
146
|
|
|
145
147
|
|
|
146
|
-
def
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
148
|
+
def _bounded(cmd, timeout, **kwargs):
|
|
149
|
+
"`bounded(cmd, timeout, merge=True, **kwargs)`, and whether it ran out of time. Outside `FASTWS_JOB`, a timeout gives a killed process's result; inside a marked job, it raises so the job's owner can clean up descendants."
|
|
150
|
+
try: return bounded(cmd, timeout, merge=True, **kwargs), False
|
|
151
|
+
except TimeoutError as e:
|
|
152
|
+
if os.environ.get(JOB_ENV): raise
|
|
153
|
+
return subprocess.CompletedProcess(cmd, -signal.SIGKILL, f'{e}\n'), True
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _suite_name(m):
|
|
157
|
+
"The `Suite` name of the test binary that Cargo artifact `m` describes: its target's source, prefixed `unittests ` unless it's an integration test."
|
|
158
|
+
name = Path(m['target']['src_path']).relative_to(Path(m['manifest_path']).parent).as_posix()
|
|
159
|
+
return name if 'test' in m['target']['kind'] else 'unittests ' + name
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _binary_id(m, packages):
|
|
163
|
+
"nextest's id for the test binary that Cargo artifact `m` describes, such as `app`, `app::more` or `app::bin/app`. `packages` maps package ids to names."
|
|
164
|
+
pkg, kind, name = packages[m['package_id']], m['target']['kind'][0], m['target']['name']
|
|
165
|
+
if kind == 'test': return f'{pkg}::{name}'
|
|
166
|
+
return f'{pkg}::{kind}/{name}' if kind in ('bin', 'example', 'bench') else pkg
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _junit(path, names):
|
|
170
|
+
"Each binary's `Suite` from nextest's JUnit report at `path`. `names` maps nextest's binary ids to suite names. Ignored tests don't appear."
|
|
171
|
+
suites = []
|
|
172
|
+
for ts in ElementTree.parse(path).getroot().iter('testsuite'):
|
|
173
|
+
tests, failures, late = {}, {}, False
|
|
174
|
+
for tc in ts.iter('testcase'):
|
|
175
|
+
t, bad = tc.get('name'), next(iter(tc.findall('failure') + tc.findall('error')), None)
|
|
176
|
+
tests[t] = 'ok' if bad is None else 'FAILED'
|
|
177
|
+
if bad is None: continue
|
|
178
|
+
failures[t] = '\n'.join(s for s in (tc.findtext('system-out', '').strip('\n'), tc.findtext('system-err', '').strip('\n') or bad.text or '') if s)
|
|
179
|
+
late |= 'timeout' in bad.get('type', '')
|
|
180
|
+
suites.append(Suite(names.get(ts.get('name'), ts.get('name')), '\n\n'.join(failures.values()), tests, failures, '', bool(failures), late))
|
|
181
|
+
return suites
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _nextest(root, args, test_args, env, timeout, artifacts, profile='default'):
|
|
185
|
+
"Run the built tests with `cargo nextest run --profile profile` under `timeout` as a whole: its exit status, its output, each binary's `Suite` and the run's seconds. Cargo's `--profile` in `args` becomes nextest's `--cargo-profile`."
|
|
186
|
+
packages = {p['id']: p['name'] for p in json.loads(cargo(root, 'metadata', '--format-version', '1', '--no-deps'))['packages']}
|
|
187
|
+
names = {_binary_id(m, packages): _suite_name(m) for m in artifacts}
|
|
188
|
+
args = [re.sub(r'^--profile(?==|$)', '--cargo-profile', a) for a in map(str, args)]
|
|
189
|
+
with tempfile.TemporaryDirectory() as d:
|
|
190
|
+
report, tool = Path(d)/'junit.xml', Path(d)/'fastws.toml'
|
|
191
|
+
tool.write_text(f'[profile.{profile}.junit]\npath = {json.dumps(str(report))}\n')
|
|
192
|
+
cmd = ['cargo', 'nextest', 'run', '--tool-config-file', f'fastws:{tool}', '--profile', profile, '--show-progress=none', '--color=never', *args, *(('--', *test_args) if test_args else ())]
|
|
193
|
+
start = time.monotonic()
|
|
194
|
+
p, late = _bounded(cmd, timeout, cwd=root, env=os.environ | env)
|
|
195
|
+
suites = _junit(report, names) if report.exists() else [Suite('nextest', p.stdout, {}, {}, '', True, late)]
|
|
196
|
+
return p.returncode, p.stdout, suites, time.monotonic() - start
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _libtest(root, args, test_args, env, timeout, artifacts):
|
|
200
|
+
"Run each built test binary in turn from its package directory under `timeout`: the first failing exit status, the output and each binary's `Suite`. Without `--no-fail-fast` in `args`, the first failing binary ends the run."
|
|
201
|
+
code, text, suites = 0, '', []
|
|
161
202
|
for m in artifacts:
|
|
162
|
-
package = Path(m['manifest_path']).parent
|
|
163
|
-
|
|
164
|
-
if 'test' not in m['target']['kind']: name = 'unittests ' + name
|
|
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
|
|
203
|
+
package, name = Path(m['manifest_path']).parent, _suite_name(m)
|
|
204
|
+
p, late = _bounded([m['executable'], *test_args], timeout, cwd=package, env=os.environ | env | {'CARGO_MANIFEST_DIR': str(package)})
|
|
167
205
|
suites.append(Suite.read(name, p.stdout.splitlines(), p.returncode, late))
|
|
168
206
|
text += f' Running {name} ({m["executable"]})\n' + p.stdout
|
|
169
207
|
if p.returncode:
|
|
170
|
-
code = p.returncode
|
|
208
|
+
code = code or p.returncode
|
|
171
209
|
if '--no-fail-fast' not in args: break
|
|
210
|
+
return code, text, suites
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _nextest_profiles(root):
|
|
214
|
+
"The nextest profiles that `root`'s `.config/nextest.toml` defines, `default` included, or None without that file."
|
|
215
|
+
config = root/'.config/nextest.toml'
|
|
216
|
+
return {'default', *tomllib.loads(config.read_text()).get('profile', {})} if config.exists() else None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def run_tests(
|
|
220
|
+
root, # A Cargo package or workspace (expands `~`), or a list of them
|
|
221
|
+
*args, # Cargo build/selection options, such as `'--test', 'core'` or `'--profile', 'checked'`; `--no-fail-fast` runs every binary
|
|
222
|
+
test_args=(), # Arguments passed directly to each test binary, such as `('name', '--nocapture')`; nextest gets them after `--`
|
|
223
|
+
env:dict=None, # Environment variables to set for the run, such as `{'BASEDPL_COSTS': '1'}`
|
|
224
|
+
timeout:float=30, # Seconds that each native test binary, or each nextest run of one profile in one root, may take after building; None: no limit
|
|
225
|
+
profiles:tuple=('default',) # The nextest profiles to run in turn, each at once in every root whose `.config/nextest.toml` defines it
|
|
226
|
+
)->Results:
|
|
227
|
+
"Build native tests once per root, with no time limit, then run Cargo's reported executables directly from their package directories. Where a root has `.config/nextest.toml`, `cargo nextest run` runs them instead, each test in its own process, once for each of `profiles` that its config defines, and each binary's results come from nextest's JUnit report. Several roots build in turn. Roots without nextest then run in turn, and each profile then runs in every root that defines it at once. Without `--no-fail-fast`, a failure ends the run after its binary, or after its profile. With several roots, each suite's name starts with its root's directory name, and with several profiles, with the profile. Source edits after building cannot trigger another build; results describe the compiled build, not the latest working tree. `timeout` bounds each binary, or each root's run of a profile, excluding compilation. Outside `FASTWS_JOB`, a run that runs out of time is reported as a failed suite with `timed_out` set. Inside a marked job, the timeout raises so its owner can clean up descendants. An omitted Cargo profile uses each root's 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."
|
|
228
|
+
roots = [Path(r).expanduser().resolve() for r in (root if isinstance(root, (list, tuple)) else [root])]
|
|
229
|
+
env = {'CARGO_TERM_QUIET': 'false', 'CARGO_TERM_COLOR': 'never'} | (env or {})
|
|
230
|
+
has_profile = any(a in ('-r', '--release', '--profile') or isinstance(a, str) and a.startswith('--profile=') for a in args)
|
|
231
|
+
text, found, built = '', [], []
|
|
232
|
+
for r in roots:
|
|
233
|
+
rargs = args if has_profile else (*args, '--profile', _dev_profile(r))
|
|
234
|
+
code, out, diags, rest = _run(r, ('test', '--no-run', *rargs), env)
|
|
235
|
+
text, found = text + out, found + diags
|
|
236
|
+
if code: return Results('test', code, text, found)
|
|
237
|
+
artifacts = [m for m in rest if m['reason'] == 'compiler-artifact' and m['profile']['test'] and m.get('executable')]
|
|
238
|
+
built.append((r, rargs, artifacts, _nextest_profiles(r)))
|
|
239
|
+
def named(r, p, suites):
|
|
240
|
+
"`suites`, freshly read, renamed for root `r` and profile `p` where several roots or profiles ran"
|
|
241
|
+
t = ' '.join(x for x in (r.name if len(roots) > 1 else '', p if p and len(profiles) > 1 else '') if x)
|
|
242
|
+
for s in suites: s.name = f'{t}: {s.name}' if t else s.name
|
|
243
|
+
return t, suites
|
|
244
|
+
fail_fast, code, suites, start = '--no-fail-fast' not in args, 0, [], time.monotonic()
|
|
245
|
+
for r, rargs, artifacts, defined in built:
|
|
246
|
+
if defined is not None: continue
|
|
247
|
+
c, out, ss = _libtest(r, rargs, test_args, env, timeout, artifacts)
|
|
248
|
+
text, suites, code = text + out, suites + named(r, None, ss)[1], code or c
|
|
249
|
+
if code and fail_fast: return Results('test', code, text, found, suites, time.monotonic() - start)
|
|
250
|
+
for p in profiles:
|
|
251
|
+
jobs = [(r, rargs, artifacts) for r, rargs, artifacts, defined in built if p in (defined or ())]
|
|
252
|
+
with ThreadPoolExecutor(max(len(jobs), 1)) as ex: runs = list(ex.map(lambda j: _nextest(j[0], j[1], test_args, env, timeout, j[2], p), jobs))
|
|
253
|
+
for (r, _, _), (c, out, ss, _) in zip(jobs, runs):
|
|
254
|
+
t, ss = named(r, p, ss)
|
|
255
|
+
text, suites, code = text + (f' Nextest profile {p} in {r}\n' if t else '') + out, suites + ss, code or c
|
|
256
|
+
if code and fail_fast: break
|
|
172
257
|
return Results('test', code, text, found, suites, time.monotonic() - start)
|
|
173
258
|
|
|
174
259
|
|
|
@@ -684,20 +684,31 @@ def _cargo_patches(root: Path):
|
|
|
684
684
|
patches[(_repo_key(url), name)] = path if path.is_absolute() else (config.parent/path).resolve()
|
|
685
685
|
return patches, config
|
|
686
686
|
|
|
687
|
-
def
|
|
688
|
-
"(`
|
|
687
|
+
def _crate_manifests(d: Path):
|
|
688
|
+
"(parsed `Cargo.toml`, dir) for the crate or workspace at `d` and each of its cargo workspace members"
|
|
689
689
|
try: data = tomllib.loads((d/"Cargo.toml").read_text())
|
|
690
690
|
except tomllib.TOMLDecodeError: return
|
|
691
|
-
|
|
691
|
+
yield data, d
|
|
692
692
|
for m in _project_dirs(d, "Cargo.toml", data.get("workspace", {}).get("members", [])):
|
|
693
693
|
try: sub = tomllib.loads((m/"Cargo.toml").read_text())
|
|
694
694
|
except tomllib.TOMLDecodeError: continue
|
|
695
|
-
|
|
695
|
+
yield sub, m
|
|
696
|
+
|
|
697
|
+
def _crate_pkgs(d: Path):
|
|
698
|
+
"(`[package]` table, dir) for the crate at `d` and its cargo workspace members"
|
|
699
|
+
for data, m in _crate_manifests(d):
|
|
700
|
+
if "name" in (pkg := data.get("package", {})): yield pkg, m
|
|
696
701
|
|
|
697
702
|
def _local_crates(root: Path) -> list[tuple[dict, Path]]:
|
|
698
703
|
"(`[package]` table, dir) for every crate under `root`, nested cargo workspace members included"
|
|
699
704
|
return [p for crate in _crate_dirs(root) for p in _crate_pkgs(crate)]
|
|
700
705
|
|
|
706
|
+
def _registry_requirements(root: Path) -> set[str]:
|
|
707
|
+
"Packages that some manifest under `root` requires from crates.io: a version with no `path`, `git`, `registry` or `workspace` key"
|
|
708
|
+
return {spec.get("package", name) if isinstance(spec, dict) else name
|
|
709
|
+
for crate in _crate_dirs(root) for data, _ in _crate_manifests(crate) for deps in _cargo_dep_tables(data, every=True)
|
|
710
|
+
for name, spec in deps.items() if isinstance(spec, str) or isinstance(spec, dict) and not {"path", "git", "registry", "workspace"} & spec.keys()}
|
|
711
|
+
|
|
701
712
|
def _git_dep_tables(root: Path, crates: dict[str, Path]) -> dict[str, dict[str, Path]]:
|
|
702
713
|
"Git URL -> {package: local dir} for members' git deps that name a local crate"
|
|
703
714
|
res = {}
|
|
@@ -741,15 +752,16 @@ def cargo_setup(root):
|
|
|
741
752
|
def _sync_cargo_patches(root: Path) -> tuple[list[str], list[str]]:
|
|
742
753
|
"""Regenerate workspace Cargo patches, default to quiet output, and return (added, removed).
|
|
743
754
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
then use the checkouts: the cargo analog of editable installs. Only
|
|
747
|
-
managed; entries pointing elsewhere (and any other config sections) are
|
|
748
|
-
over a generated one of the same name."""
|
|
755
|
+
A local crate gets a `[patch.crates-io]` entry only where some local manifest requires it from crates.io
|
|
756
|
+
(`_registry_requirements`); Cargo uses no other patch. A member's git dep on a local crate gets an entry under
|
|
757
|
+
that URL. Builds anywhere under `root` then use the checkouts: the cargo analog of editable installs. Only
|
|
758
|
+
entries whose path is inside `root` are managed; entries pointing elsewhere (and any other config sections) are
|
|
759
|
+
kept as-is, and a kept entry wins over a generated one of the same name."""
|
|
749
760
|
root = root.resolve()
|
|
750
761
|
pkgs = _local_crates(root)
|
|
751
762
|
crates = {pkg["name"]: d for pkg, d in pkgs}
|
|
752
|
-
|
|
763
|
+
required = _registry_requirements(root)
|
|
764
|
+
desired = {"crates-io": {name: str(d) for name, d in crates.items() if name in required}}
|
|
753
765
|
for url, entries in _git_dep_tables(root, crates).items(): desired[url] = {name: str(d) for name, d in entries.items()}
|
|
754
766
|
config = root/".cargo"/"config.toml"
|
|
755
767
|
original = content = config.read_text() if config.exists() else ""
|
|
@@ -816,10 +828,13 @@ def _sync_cargo_wrapper(root: Path):
|
|
|
816
828
|
config.parent.mkdir(exist_ok=True)
|
|
817
829
|
config.write_text(new)
|
|
818
830
|
|
|
819
|
-
def _cargo_dep_tables(data):
|
|
820
|
-
|
|
831
|
+
def _cargo_dep_tables(data, every=False):
|
|
832
|
+
"A manifest's dependency and build-dependency tables, top-level and per target; with `every`, its dev-dependency tables and `[workspace.dependencies]` too"
|
|
833
|
+
names = ("dependencies", "build-dependencies", "dev-dependencies")[:3 if every else 2]
|
|
834
|
+
for name in names: yield data.get(name, {})
|
|
821
835
|
for target in data.get("target", {}).values():
|
|
822
|
-
for name in
|
|
836
|
+
for name in names: yield target.get(name, {})
|
|
837
|
+
if every: yield data.get("workspace", {}).get("dependencies", {})
|
|
823
838
|
|
|
824
839
|
def _patched_cargo_deps(crate: Path, patches):
|
|
825
840
|
data = tomllib.loads((crate/"Cargo.toml").read_text())
|
|
@@ -942,8 +957,10 @@ def _npm_install(root: Path):
|
|
|
942
957
|
except json.JSONDecodeError:
|
|
943
958
|
print(res.stdout, file=sys.stderr, end="")
|
|
944
959
|
raise
|
|
945
|
-
|
|
946
|
-
|
|
960
|
+
findings = {}
|
|
961
|
+
if data.get("audit", {}).get("vulnerabilities", {}).get("total"): findings["audit"] = data["audit"]
|
|
962
|
+
if data.get("unreviewedScripts"): findings["unreviewedScripts"] = data["unreviewedScripts"]
|
|
963
|
+
if findings: print(f"npm install findings ({root}):\n{json.dumps(findings, indent=2)}", file=sys.stderr)
|
|
947
964
|
|
|
948
965
|
def _sync_js(root: Path, members: list[Path]) -> list[Path]:
|
|
949
966
|
"Install the JS workspace, run every native member's build script, and return those members. Cargo handles incremental compilation."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastws-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.35
|
|
4
4
|
Summary: Fast workspace tools for multi-repo management
|
|
5
5
|
Author: Jeremy Howard
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -256,7 +256,11 @@ Use it when another process's unfinished edits stop the workspace compiling, suc
|
|
|
256
256
|
|
|
257
257
|
### Rust builds and tests from Python
|
|
258
258
|
|
|
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.
|
|
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. Outside `FASTWS_JOB`, a timed-out binary is reported as a failed suite with `timed_out` set. Inside a marked job, its timeout raises so the job owner can clean up descendants. 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.
|
|
260
|
+
|
|
261
|
+
Where a root has `.config/nextest.toml`, `cargo nextest run` runs the built tests instead: each test in its own process, every binary at once, with the config's slow-test timeouts and groups. `profiles` lists the nextest profiles to run one after another, `('default',)` unless given. `timeout` bounds each profile's run, and each binary's results come from nextest's JUnit report, which lists no ignored tests. `--profile NAME` passes to nextest as `--cargo-profile NAME`, and `test_args` follow `--`.
|
|
262
|
+
|
|
263
|
+
`root` can also be a list of roots, which build one after another. Roots without nextest then run their binaries in turn. Each profile then runs at the same time in every root whose config defines it: tests in different roots share a phase, and no phase starts before the previous one ends. Suite names start with the root's directory name, and with several profiles also with the profile, as in `app timed: tests/speed.rs`. A failure ends the run after its profile unless `--no-fail-fast` is given.
|
|
260
264
|
|
|
261
265
|
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.
|
|
262
266
|
|
|
@@ -265,6 +269,7 @@ from fastws.cargo import run_tests
|
|
|
265
269
|
|
|
266
270
|
native = run_tests('.', '--no-fail-fast')
|
|
267
271
|
focused = run_tests('.', '--test', 'core', test_args=('test_name', '--nocapture'))
|
|
272
|
+
phased = run_tests(['app', 'lib'], profiles=('untimed', 'timed'))
|
|
268
273
|
```
|
|
269
274
|
|
|
270
275
|
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.
|
|
@@ -314,7 +319,7 @@ Fastws maintains `tool.uv.workspace.exclude`. It excludes unlisted top-level dir
|
|
|
314
319
|
|
|
315
320
|
Existing globs, entries for missing directories, and entries for checkouts that are still not Python projects are retained. Use `exclude = [...]` under `[tool.fastws]` to specify exclusions the scan cannot infer, such as keeping a valid project out of the workspace. Adding members preserves hand-written `[tool.uv.sources]` entries, including path and Git sources.
|
|
316
321
|
|
|
317
|
-
Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `[patch.crates-io]` gets an entry for each local crate, including nested Cargo workspace members,
|
|
322
|
+
Each sync regenerates Cargo overrides in the workspace's `.cargo/config.toml`. `[patch.crates-io]` gets an entry for each local crate, including nested Cargo workspace members, that some local manifest requires from crates.io: by version alone, without `path`, `git` or `workspace`, in any dependency table or `[workspace.dependencies]`. Git dependencies that name local crates get entries under the matching URL. Builds under the workspace root use these local checkouts. Entries pointing outside the root and other configuration sections are preserved.
|
|
318
323
|
|
|
319
324
|
Sync also defaults `[term] quiet = true` in that file to suppress routine Cargo output and warnings. An explicit `quiet = false` is preserved.
|
|
320
325
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import subprocess, pytest, fastws.core as core
|
|
1
|
+
import shutil, subprocess, pytest, fastws.core as core
|
|
2
2
|
from fastws.cargo import build, check, run_tests, Suite
|
|
3
3
|
|
|
4
4
|
|
|
@@ -46,10 +46,10 @@ def test_build_diagnostics(tmp_path, capfd, body, diagnostic):
|
|
|
46
46
|
assert checked.ok == (diagnostic != 'error') and [(d.path, d.level) for d in checked.diagnostics] == ([('dep/src/lib.rs', diagnostic)] if diagnostic else [])
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def
|
|
50
|
-
(
|
|
51
|
-
(
|
|
52
|
-
(
|
|
49
|
+
def _test_crate(root):
|
|
50
|
+
(root/'Cargo.toml').write_text('[package]\nname = "app"\nversion = "0.1.0"\nedition = "2021"\n')
|
|
51
|
+
(root/'src').mkdir()
|
|
52
|
+
(root/'src/lib.rs').write_text('''pub fn unused() { let x = 1; }
|
|
53
53
|
pub fn two() -> i32 { 2 }
|
|
54
54
|
#[cfg(test)] mod t {
|
|
55
55
|
#[test] fn passes() {}
|
|
@@ -60,10 +60,14 @@ pub fn two() -> i32 { 2 }
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
''')
|
|
63
|
-
(
|
|
64
|
-
(
|
|
65
|
-
(
|
|
66
|
-
(
|
|
63
|
+
(root/'tests').mkdir()
|
|
64
|
+
(root/'tests/more.rs').write_text('#[test] fn other() { assert_eq!(app::two(), 2); }\n')
|
|
65
|
+
(root/'.cargo').mkdir()
|
|
66
|
+
(root/'.cargo/config.toml').write_text('[term]\nquiet = true\n')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_run_tests(tmp_path):
|
|
70
|
+
_test_crate(tmp_path)
|
|
67
71
|
r = run_tests(tmp_path, '--no-fail-fast')
|
|
68
72
|
assert not r.ok
|
|
69
73
|
assert [(d.path, d.line, d.level, d.code) for d in r.diagnostics] == [('src/lib.rs', 1, 'warning', 'unused_variables')]
|
|
@@ -85,6 +89,43 @@ pub fn two() -> i32 { 2 }
|
|
|
85
89
|
assert run_tests(tmp_path, '--test', 'more', *args).ok
|
|
86
90
|
|
|
87
91
|
|
|
92
|
+
@pytest.mark.skipif(not shutil.which('cargo-nextest'), reason='needs cargo-nextest')
|
|
93
|
+
def test_run_tests_nextest(tmp_path):
|
|
94
|
+
_test_crate(tmp_path)
|
|
95
|
+
(tmp_path/'.config').mkdir()
|
|
96
|
+
(tmp_path/'.config/nextest.toml').write_text('[profile.default]\nfail-fast = false\n')
|
|
97
|
+
r = run_tests(tmp_path)
|
|
98
|
+
assert not r.ok and [(d.path, d.level) for d in r.diagnostics] == [('src/lib.rs', 'warning')]
|
|
99
|
+
suites = {s.name: s for s in r.suites}
|
|
100
|
+
unit, more = suites['unittests src/lib.rs'], suites['tests/more.rs']
|
|
101
|
+
assert unit.tests == {'t::passes': 'ok', 't::fails': 'FAILED'} and unit.failed and 'broken' in unit.failures['t::fails']
|
|
102
|
+
assert more.tests == {'other': 'ok'} and not more.failed
|
|
103
|
+
assert run_tests(tmp_path, '--test', 'more', '--profile', 'test').ok
|
|
104
|
+
slow = run_tests(tmp_path, '--lib', test_args=('skipped', '--ignored'), timeout=5)
|
|
105
|
+
s, = slow.suites
|
|
106
|
+
assert not slow.ok and s.timed_out and str(s) == 'nextest: ran out of time'
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.mark.skipif(not shutil.which('cargo-nextest'), reason='needs cargo-nextest')
|
|
110
|
+
def test_run_tests_profiles(tmp_path):
|
|
111
|
+
a, b = tmp_path/'a', tmp_path/'b'
|
|
112
|
+
for d in a, b: (d/'.config').mkdir(parents=True)
|
|
113
|
+
_test_crate(a)
|
|
114
|
+
(a/'.config/nextest.toml').write_text('[profile.one]\ndefault-filter = "test(passes) | test(other)"\n[profile.two]\ndefault-filter = "test(fails)"\n')
|
|
115
|
+
(b/'Cargo.toml').write_text('[package]\nname = "b"\nversion = "0.1.0"\nedition = "2021"\n')
|
|
116
|
+
(b/'src').mkdir()
|
|
117
|
+
(b/'src/lib.rs').write_text('')
|
|
118
|
+
(b/'tests').mkdir()
|
|
119
|
+
(b/'tests/fine.rs').write_text('#[test] fn fine() {}\n')
|
|
120
|
+
(b/'.config/nextest.toml').write_text('[profile.one]\n')
|
|
121
|
+
r = run_tests([a, b], profiles=('one', 'two'))
|
|
122
|
+
tests = {s.name: s.tests for s in r.suites if s.tests}
|
|
123
|
+
assert not r.ok and tests == {'a one: unittests src/lib.rs': {'t::passes': 'ok'}, 'a one: tests/more.rs': {'other': 'ok'},
|
|
124
|
+
'b one: tests/fine.rs': {'fine': 'ok'}, 'a two: unittests src/lib.rs': {'t::fails': 'FAILED'}}
|
|
125
|
+
first = run_tests([a, b], profiles=('two', 'one'))
|
|
126
|
+
assert not first.ok and {s.name for s in first.suites if s.tests} == {'a two: unittests src/lib.rs'}
|
|
127
|
+
|
|
128
|
+
|
|
88
129
|
def test_captured_test_output():
|
|
89
130
|
for failed in (False, True):
|
|
90
131
|
lines = ['test passes ... ok', 'successes:', '---- passes stdout ----', 'printed', 'successes:', 'passes']
|
|
@@ -27,6 +27,8 @@ def test_config_points_only_the_roots_crates_at_the_root(tmp_path):
|
|
|
27
27
|
f'[build]\nrustc-wrapper = "/lock"\n\n[patch.crates-io]\na = {{ path = "{workspace}/a" }}\nb = {{ path = "{workspace}/b" }}\n')
|
|
28
28
|
(root/"a").mkdir(parents=True)
|
|
29
29
|
(root/"a"/"Cargo.toml").write_text('[package]\nname = "a"\nversion = "0.1.0"\n')
|
|
30
|
+
(root/"c").mkdir()
|
|
31
|
+
(root/"c"/"Cargo.toml").write_text('[package]\nname = "c"\nversion = "0.1.0"\n\n[dependencies]\na = "0.1"\n')
|
|
30
32
|
_config(root, workspace)
|
|
31
33
|
config = tomllib.loads((root/".cargo"/"config.toml").read_text())
|
|
32
34
|
assert config["build"]["rustc-wrapper"] == "/lock"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fcntl, os, subprocess, sys, pytest
|
|
1
|
+
import fcntl, os, subprocess, sys, threading, pytest
|
|
2
2
|
from fastws.procs import JOB_ENV, kill_group
|
|
3
3
|
|
|
4
4
|
|
|
@@ -25,10 +25,17 @@ time.sleep(60)
|
|
|
25
25
|
'''
|
|
26
26
|
with subprocess.Popen([sys.executable, '-c', job], start_new_session=True, env=os.environ | {JOB_ENV: '1'},
|
|
27
27
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as p:
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
lines, survived = [], threading.Event()
|
|
29
|
+
def read():
|
|
30
|
+
for line in p.stdout:
|
|
31
|
+
lines.append(line)
|
|
32
|
+
if line.startswith('job survived inner timeout'): survived.set()
|
|
33
|
+
reader = threading.Thread(target=read, daemon=True)
|
|
34
|
+
reader.start()
|
|
35
|
+
try: assert survived.wait(10), ''.join(lines)
|
|
30
36
|
finally: kill_group(p)
|
|
31
|
-
|
|
37
|
+
reader.join(2)
|
|
38
|
+
output = ''.join(lines)
|
|
32
39
|
group = output.split('job group ')[1].split()[0]
|
|
33
40
|
assert f'child group {group} 1' in output and f'descendant group {group}' in output
|
|
34
41
|
assert 'job survived inner timeout' in output
|
|
@@ -171,24 +171,27 @@ def test_sync_cargo_patches_generates_and_preserves(tmp_path):
|
|
|
171
171
|
config = tmp_path/'.cargo'/'config.toml'
|
|
172
172
|
config.write_text('[term]\nquiet = true\n\n[patch.crates-io]\n'
|
|
173
173
|
f'foreign = {{ path = "/elsewhere/foreign" }}\ngone = {{ path = "{tmp_path}/gone" }}\n')
|
|
174
|
-
crate1 = tmp_path/'crate1'
|
|
175
|
-
crate1.mkdir()
|
|
176
|
-
(crate1/'Cargo.toml').write_text('[package]\nname = "crate1"\nversion = "0.1.0"\n\n[dependencies]\nfamily = { git = "https://example.com/family" }\n'
|
|
174
|
+
crate1, helper = tmp_path/'crate1', tmp_path/'helper'
|
|
175
|
+
crate1.mkdir(); helper.mkdir()
|
|
176
|
+
(crate1/'Cargo.toml').write_text('[package]\nname = "crate1"\nversion = "0.1.0"\n\n[dependencies]\nfamily = { git = "https://example.com/family" }\n'
|
|
177
|
+
'\n[dev-dependencies]\nfamily-sub = { version = "0.1" }\n')
|
|
178
|
+
(helper/'Cargo.toml').write_text('[package]\nname = "helper"\nversion = "0.1.0"\n')
|
|
177
179
|
family = tmp_path/'family'
|
|
178
180
|
(family/'sub').mkdir(parents=True)
|
|
179
|
-
(family/'Cargo.toml').write_text('[package]\nname = "family"\nversion = "0.1.0"\npublish = false\n\n[workspace]\nmembers = ["sub"]\n'
|
|
181
|
+
(family/'Cargo.toml').write_text('[package]\nname = "family"\nversion = "0.1.0"\npublish = false\n\n[workspace]\nmembers = ["sub"]\n'
|
|
182
|
+
'\n[workspace.dependencies]\nhelper = "0.1"\ncrate1 = { path = "../crate1", version = "0.1" }\n')
|
|
180
183
|
(family/'sub'/'Cargo.toml').write_text('[package]\nname = "family-sub"\nversion = "0.1.0"\n')
|
|
181
184
|
|
|
182
185
|
added, removed = core._sync_cargo_patches(tmp_path)
|
|
183
186
|
data = core.tomllib.loads(config.read_text())
|
|
184
187
|
cio = data['patch']['crates-io']
|
|
185
|
-
assert set(cio) == {'foreign', '
|
|
188
|
+
assert set(cio) == {'foreign', 'family-sub', 'helper'} # only crates that a local manifest requires from crates.io; `crate1` is required by path
|
|
186
189
|
assert cio['foreign']['path'] == '/elsewhere/foreign' # entries pointing outside the root are kept as-is
|
|
187
|
-
assert cio['
|
|
188
|
-
assert cio['
|
|
190
|
+
assert cio['family-sub']['path'] == str(family/'sub') # required as a dev-dependency
|
|
191
|
+
assert cio['helper']['path'] == str(helper) # required in `[workspace.dependencies]`
|
|
189
192
|
assert data['patch']['https://example.com/family']['family']['path'] == str(family) # git deps on local crates get their URL table, published or not
|
|
190
193
|
assert data['term']['quiet'] is True # other sections untouched
|
|
191
|
-
assert set(added) == {'
|
|
194
|
+
assert set(added) == {'family', 'family-sub', 'helper'} and removed == ['gone']
|
|
192
195
|
|
|
193
196
|
|
|
194
197
|
def test_sync_ws_package_json_preserves_local_and_merges_shared(tmp_path):
|
|
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
|