mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 05:57:09 -06:00
Merge storytold/main into fix/2553-gif-bmp-header
Picks up #2667 (the shared Linux gate) so this PR runs it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PtjHGt5gnCHXgLt4NbGorf
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
name: CI
|
||||
|
||||
# Quality gates for every push to main and every pull request (`cargo xtask ci`), through the
|
||||
# suite's shared gate workflow in craft-common-actions.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# A newer push to a PR branch cancels its older run; every push to main runs to the end.
|
||||
concurrency:
|
||||
group: ci-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
# Tracks @main so suite-wide CI fixes reach every app without a PR in each repository.
|
||||
uses: storytold/craft-common-actions/.github/workflows/gate.yml@main
|
||||
with:
|
||||
# Same craft-fonts commit as release.yml.
|
||||
craft-fonts-ref: a71c20e30af613287c504f7e685ca8fad77752bc
|
||||
timeout-minutes: 90
|
||||
# xtask ci runs cargo test: the demo scene cache is per process, so nextest is slower here.
|
||||
tools: ""
|
||||
packages: >-
|
||||
pkg-config libxkbcommon-dev libwayland-dev libx11-dev libxcb1-dev libxcursor-dev
|
||||
libxrandr-dev libxi-dev libfontconfig1-dev libfreetype-dev mesa-vulkan-drivers
|
||||
libgl1-mesa-dev libdbus-1-dev
|
||||
@@ -1,23 +1,18 @@
|
||||
# FreeBSD build and package check in a QEMU VM.
|
||||
name: FreeBSD
|
||||
|
||||
# The VM build is slow, so it runs nightly on main and on pushes to `release`, not on every pull
|
||||
# request: a drifted dependency still shows up within a day, and always before a release ships.
|
||||
# PRs that change the FreeBSD workflow or packaging itself still run it.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- ".github/workflows/freebsd.yml"
|
||||
- "packaging/freebsd/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "crates/**"
|
||||
branches: [release]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- ".github/workflows/freebsd.yml"
|
||||
- "packaging/freebsd/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "crates/**"
|
||||
- .github/workflows/freebsd.yml
|
||||
- packaging/freebsd/**
|
||||
schedule:
|
||||
- cron: "7 4 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -18,6 +18,44 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
git-sources:
|
||||
name: Git dependencies come from storytold
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Git dependencies come from storytold
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }
|
||||
# Source replacement, patches and path overrides in cargo config never show up
|
||||
# in Cargo.lock. Parsed rather than grepped: TOML has many spellings of one key.
|
||||
# Cargo still reads the legacy .cargo/config.
|
||||
python3 - <<'PY'
|
||||
import os, sys, tomllib
|
||||
for path in (".cargo/config", ".cargo/config.toml"):
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
config = tomllib.load(open(path, "rb"))
|
||||
replaced = [name for name, source in config.get("source", {}).items()
|
||||
if isinstance(source, dict) and "replace-with" in source]
|
||||
if replaced or "patch" in config or "paths" in config:
|
||||
print(f"{path} replaces, patches or overrides a source")
|
||||
sys.exit(1)
|
||||
PY
|
||||
# Fails when Cargo.lock is out of date with the manifests.
|
||||
cargo metadata --locked --format-version 1 > /dev/null
|
||||
python3 - <<'PY'
|
||||
import sys, tomllib
|
||||
lock = tomllib.load(open("Cargo.lock", "rb"))
|
||||
bad = sorted({p["source"] for p in lock.get("package", [])
|
||||
if p.get("source", "").startswith("git+")
|
||||
and not p["source"].startswith("git+https://github.com/storytold/")})
|
||||
if bad:
|
||||
print("Git dependencies outside github.com/storytold:", *bad, sep="\n")
|
||||
sys.exit(1)
|
||||
PY
|
||||
|
||||
nix:
|
||||
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
|
||||
uses: storytold/craft-common-actions/.github/workflows/nix.yml@main
|
||||
|
||||
@@ -20,7 +20,47 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
git-sources:
|
||||
name: Git dependencies come from storytold
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Git dependencies come from storytold
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -f Cargo.lock || { echo "Cargo.lock is missing"; exit 1; }
|
||||
# Source replacement, patches and path overrides in cargo config never show up
|
||||
# in Cargo.lock. Parsed rather than grepped: TOML has many spellings of one key.
|
||||
# Cargo still reads the legacy .cargo/config.
|
||||
python3 - <<'PY'
|
||||
import os, sys, tomllib
|
||||
for path in (".cargo/config", ".cargo/config.toml"):
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
config = tomllib.load(open(path, "rb"))
|
||||
replaced = [name for name, source in config.get("source", {}).items()
|
||||
if isinstance(source, dict) and "replace-with" in source]
|
||||
if replaced or "patch" in config or "paths" in config:
|
||||
print(f"{path} replaces, patches or overrides a source")
|
||||
sys.exit(1)
|
||||
PY
|
||||
# Fails when Cargo.lock is out of date with the manifests.
|
||||
cargo metadata --locked --format-version 1 > /dev/null
|
||||
python3 - <<'PY'
|
||||
import sys, tomllib
|
||||
lock = tomllib.load(open("Cargo.lock", "rb"))
|
||||
bad = sorted({p["source"] for p in lock.get("package", [])
|
||||
if p.get("source", "").startswith("git+")
|
||||
and not p["source"].startswith("git+https://github.com/storytold/")})
|
||||
if bad:
|
||||
print("Git dependencies outside github.com/storytold:", *bad, sep="\n")
|
||||
sys.exit(1)
|
||||
PY
|
||||
|
||||
release:
|
||||
needs: git-sources
|
||||
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
|
||||
uses: storytold/craft-common-actions/.github/workflows/release.yml@main
|
||||
with:
|
||||
|
||||
@@ -5,23 +5,17 @@
|
||||
# on GitHub Actions' native windows-11-arm runner.
|
||||
name: Windows ARM64
|
||||
|
||||
# Nightly on main and on pushes to `release` (dependency and runner-image drift shows up within a
|
||||
# day, and always before a release ships), and on PRs that change the Windows packaging itself.
|
||||
on:
|
||||
schedule:
|
||||
- cron: "3 5 * * *"
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- ".github/workflows/windows-arm64.yml"
|
||||
- "packaging/windows/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "crates/**"
|
||||
branches: [release]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- ".github/workflows/windows-arm64.yml"
|
||||
- "packaging/windows/**"
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
- "crates/**"
|
||||
- .github/workflows/windows-arm64.yml
|
||||
- packaging/windows/**
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
||||
Generated
+33
-33
@@ -3087,7 +3087,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"eframe",
|
||||
@@ -3113,7 +3113,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-catalog"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"crc32fast",
|
||||
@@ -3131,7 +3131,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-cli"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"dhat",
|
||||
"lightcraft-codecs",
|
||||
@@ -3147,7 +3147,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-codecs"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"avif-serialize",
|
||||
"bytemuck",
|
||||
@@ -3178,7 +3178,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-color"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"lightcraft-geom",
|
||||
"proptest",
|
||||
@@ -3187,7 +3187,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-denoise"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"faer-core",
|
||||
"lightcraft-denoise-core",
|
||||
@@ -3204,7 +3204,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-denoise-core"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"lightcraft-raster",
|
||||
"miniz_oxide 0.8.9",
|
||||
@@ -3219,7 +3219,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-develop"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"lightcraft-color",
|
||||
"lightcraft-geom",
|
||||
@@ -3231,7 +3231,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-engine"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"chrono",
|
||||
@@ -3275,7 +3275,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-faces"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"lightcraft-testkit",
|
||||
"matrixmultiply",
|
||||
@@ -3290,7 +3290,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-fetch"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"rustls",
|
||||
@@ -3301,7 +3301,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-geocode"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"miniz_oxide 0.8.9",
|
||||
"serde",
|
||||
@@ -3309,7 +3309,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-geom"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -3317,7 +3317,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-gpu"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"lightcraft-color",
|
||||
@@ -3337,7 +3337,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-heif"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"heic-rs",
|
||||
"rayon",
|
||||
@@ -3345,14 +3345,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-inpaint"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-lensdb"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"lightcraft-develop",
|
||||
"lightcraft-geom",
|
||||
@@ -3361,7 +3361,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-mcp"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"lightcraft-codecs",
|
||||
"lightcraft-engine",
|
||||
@@ -3372,7 +3372,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-merge"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"lightcraft-codecs",
|
||||
"lightcraft-color",
|
||||
@@ -3388,7 +3388,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-meta"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lightcraft-geom",
|
||||
@@ -3401,7 +3401,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-pipeline"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"lightcraft-color",
|
||||
@@ -3419,7 +3419,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-preview"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"jpeg-encoder",
|
||||
"lightcraft-raster",
|
||||
@@ -3431,7 +3431,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-raster"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"lightcraft-color",
|
||||
@@ -3443,7 +3443,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-raw"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"dhat",
|
||||
"jpeg-encoder",
|
||||
@@ -3467,7 +3467,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-scenes"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"lightcraft-color",
|
||||
@@ -3478,7 +3478,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-segment"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"candle-core",
|
||||
"candle-nn",
|
||||
@@ -3492,7 +3492,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-slideshow"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"lightcraft-raster",
|
||||
"serde",
|
||||
@@ -3501,15 +3501,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-sysmem"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-testkit"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-tiff"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -3518,7 +3518,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-ui-egui"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"craft-ui",
|
||||
"egui",
|
||||
@@ -3547,7 +3547,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lightcraft-web"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"eframe",
|
||||
@@ -7017,7 +7017,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"flate2",
|
||||
|
||||
+3
-1
@@ -3,7 +3,7 @@ resolver = "3"
|
||||
members = ["crates/*", "apps/*", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.11.0"
|
||||
version = "0.13.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.90"
|
||||
@@ -125,9 +125,11 @@ excessive_precision = "allow"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
debug = false
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
debug = "line-tables-only"
|
||||
|
||||
[profile.release]
|
||||
debug = "line-tables-only"
|
||||
|
||||
@@ -410,10 +410,12 @@ cargo xtask ci # fmt, clippy, HEIF, tes
|
||||
CI defaults to line-table debug information and one build job per 1.5 GB of RAM
|
||||
available when it starts (at most one per CPU; 4 if memory can't be read), so an
|
||||
8 GB machine with 6 GB free builds with 4, and a busy machine with little free
|
||||
builds with fewer. Test threads follow, capped at 4. Full-debuginfo linkers, one
|
||||
per CPU, otherwise exhaust RAM before any test runs. GPU coverage is unchanged. Explicit
|
||||
`CARGO_PROFILE_DEV_DEBUG`, `CARGO_BUILD_JOBS` and `RUST_TEST_THREADS` settings
|
||||
override these defaults. Ordinary development commands keep their own settings.
|
||||
builds with fewer. Test threads follow, capped at 4.
|
||||
Full-debuginfo linkers, one per CPU, otherwise exhaust RAM before any test runs. GPU coverage is
|
||||
unchanged. Explicit `CARGO_PROFILE_DEV_DEBUG`, `CARGO_BUILD_JOBS`, `RUST_TEST_THREADS` and
|
||||
`NEXTEST_TEST_THREADS` settings override these defaults. The tests run under
|
||||
`cargo test`; `LIGHTCRAFT_NEXTEST=1` runs them under [cargo-nextest](https://nexte.st) instead (one
|
||||
process per test, slower here because each regenerates the demo scenes). Ordinary development commands keep their own settings.
|
||||
|
||||
**Release builds on memory-constrained machines:** [#1196](https://github.com/storytold/lightcraft/issues/1196)
|
||||
reports Arch/AUR release-link OOM kills on a 16 GB machine when the desktop and CLI build concurrently
|
||||
|
||||
+1
-1
@@ -357,7 +357,7 @@ CI 默认使用行表调试信息,并按启动时可用内存每 1.5 GB 开一
|
||||
(每个 CPU 最多一个;读不到内存时为 4 个),所以一台 8 GB、空闲 6 GB 的机器会用 4 个任务构建,
|
||||
而一台空闲内存很少的繁忙机器会用更少。测试线程数随之调整,上限为 4。否则,每个 CPU 一个的
|
||||
完整调试信息链接器会在任何测试运行之前就耗尽内存。GPU 覆盖范围不变。显式设置
|
||||
`CARGO_PROFILE_DEV_DEBUG`、`CARGO_BUILD_JOBS` 和 `RUST_TEST_THREADS` 会覆盖这些默认值。
|
||||
`CARGO_PROFILE_DEV_DEBUG`、`CARGO_BUILD_JOBS`、`RUST_TEST_THREADS` 和 `NEXTEST_TEST_THREADS` 会覆盖这些默认值。
|
||||
日常开发命令保持它们各自的设置。
|
||||
|
||||
**内存受限机器上的 release 构建:** [#1196](https://github.com/storytold/lightcraft/issues/1196)
|
||||
|
||||
+1
-1
@@ -375,7 +375,7 @@ CI 預設使用行表除錯資訊,並依啟動時可用記憶體每 1.5 GB 開
|
||||
(每個 CPU 最多一個;讀不到記憶體時為 4 個),所以一台 8 GB、剩餘 6 GB 可用的機器會用 4 個任務建置,
|
||||
而一台忙碌、剩餘記憶體很少的機器會用更少。測試執行緒數隨之調整,上限為 4。若不限制,每個 CPU 各跑
|
||||
一個帶完整除錯資訊的連結器,測試還沒開始記憶體就會耗盡。GPU 測試的涵蓋範圍不受影響。明確設定
|
||||
`CARGO_PROFILE_DEV_DEBUG`、`CARGO_BUILD_JOBS` 與 `RUST_TEST_THREADS` 會覆寫這些預設值。
|
||||
`CARGO_PROFILE_DEV_DEBUG`、`CARGO_BUILD_JOBS`、`RUST_TEST_THREADS` 與 `NEXTEST_TEST_THREADS` 會覆寫這些預設值。
|
||||
日常開發指令保留各自的設定。
|
||||
|
||||
**記憶體受限機器上的 release 建置:** [#1196](https://github.com/storytold/lightcraft/issues/1196)
|
||||
|
||||
+2
-2
@@ -121,7 +121,7 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
|
||||
RGGB and GBRG, as-shot WB from `WBG`); DiMAGE 7, 7Hi, A1, A2, A200 and Dynax / Maxxum 7D verified on
|
||||
CC0 files. 551 models verified, 201 preview-only; headline numbers unchanged.
|
||||
- 2026-10-14: 97 Canon CR2, Pentax and Ricoh models verified, one CC0 raw.pixls.us file each pinned and
|
||||
checked against the camera JPEG (`crates/raw/tests/canon_pentax_corpus.rs`). 334 models verified, ~50% of
|
||||
checked against the camera JPEG (`crates/raw/tests/it/canon_pentax_corpus.rs`). 334 models verified, ~50% of
|
||||
photographers on a verified body; headline numbers unchanged.
|
||||
- 2026-10-12: Mamiya MEF (ZD) decodes from sensor data and is imported; verified on a CC0 file. 241 models
|
||||
verified, 35 unsupported; headline numbers unchanged.
|
||||
@@ -135,7 +135,7 @@ Ranked toward beta; detail in [docs/roadmap.md](docs/roadmap.md), gaps in [docs/
|
||||
decode. The EOS R6 Mark III gets its as-shot white balance (`ColorData` 66). 259 models verified; RAW
|
||||
decoding 77% → 83%; mainstream ~49% → ~51%, essentials ~64% → ~65%, full ~61%.
|
||||
- 2026-10-12: 117 Nikon and Fujifilm bodies verified, one CC0 raw.pixls.us file each pinned and checked
|
||||
against the camera JPEG (`crates/raw/tests/body_corpus.rs`); the sweep found COOLPIX 5700 / 8400 / 8800
|
||||
against the camera JPEG (`crates/raw/tests/it/body_corpus.rs`); the sweep found COOLPIX 5700 / 8400 / 8800
|
||||
decoding wrongly. 354 models verified, ~58% of photographers on a verified body; headline numbers unchanged.
|
||||
- 2026-10-11: Canon EOS R5 Mark II C-RAW decodes from sensor data (57% of its files opened as a
|
||||
preview): the CRX QP map's first-symbol literal and limited-length escape; four CC0 R5 Mark II files
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
|
||||
mod cli;
|
||||
mod mcp_shutdown;
|
||||
@@ -240,11 +240,14 @@ mod tests {
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
fn new(open: &Arc<AtomicUsize>, idle: Duration) -> Self {
|
||||
/// `before` acts on the client before the worker starts reading, so what it sends is
|
||||
/// already there however late this thread gets scheduled.
|
||||
fn new(open: &Arc<AtomicUsize>, idle: Duration, before: impl FnOnce(&mut TcpStream)) -> Self {
|
||||
let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
|
||||
let client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
|
||||
let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
|
||||
client.set_read_timeout(Some(Duration::from_secs(3))).unwrap();
|
||||
let (stream, _) = listener.accept().unwrap();
|
||||
before(&mut client);
|
||||
let (tx, requests) = channel();
|
||||
let worker = spawn_connection(stream, tx, egui::Context::default(), Arc::clone(open), idle).unwrap();
|
||||
Self { client: BufReader::new(client), requests, worker: Some(worker) }
|
||||
@@ -271,9 +274,8 @@ mod tests {
|
||||
fn idle_connections_release_their_slots_without_dispatching() {
|
||||
for input in ["", "{\"method\":\"engine.execute\""] {
|
||||
let open = Arc::new(AtomicUsize::new(0));
|
||||
let mut connection = Connection::new(&open, Duration::from_millis(100));
|
||||
let mut connection = Connection::new(&open, Duration::from_millis(100), |c| c.write_all(input.as_bytes()).unwrap());
|
||||
assert_eq!(open.load(Ordering::SeqCst), 1);
|
||||
connection.client.get_mut().write_all(input.as_bytes()).unwrap();
|
||||
let mut reply = String::new();
|
||||
let closed = connection.client.read_to_string(&mut reply);
|
||||
assert!(closed.is_ok(), "idle connection stayed open: {closed:?}");
|
||||
@@ -285,8 +287,7 @@ mod tests {
|
||||
assert!(reply["error"].as_str().unwrap().contains("idle read timed out"));
|
||||
|
||||
// A subsequent connection is admitted through the same accounting path.
|
||||
let mut next = Connection::new(&open, Duration::from_secs(1));
|
||||
next.client.get_mut().shutdown(std::net::Shutdown::Write).unwrap();
|
||||
let mut next = Connection::new(&open, Duration::from_secs(1), |c| c.shutdown(std::net::Shutdown::Write).unwrap());
|
||||
let mut eof = String::new();
|
||||
next.client.read_to_string(&mut eof).unwrap();
|
||||
next.join();
|
||||
@@ -298,12 +299,20 @@ mod tests {
|
||||
#[test]
|
||||
fn persistent_requests_and_slow_replies_outlive_the_idle_read_timeout() {
|
||||
let open = Arc::new(AtomicUsize::new(0));
|
||||
let mut connection = Connection::new(&open, Duration::from_millis(100));
|
||||
let request = |id: u64| format!("{}\n", json!({"id": id, "method": "engine.commands"}));
|
||||
// Each request reaches the socket before the worker reads it (the next one while the worker
|
||||
// waits for the reply), so only the reply waits outlast the idle timeout, however slowly
|
||||
// this thread runs.
|
||||
let mut connection = Connection::new(&open, Duration::from_millis(100), |c| c.write_all(request(1).as_bytes()).unwrap());
|
||||
for id in [1, 2] {
|
||||
writeln!(connection.client.get_mut(), "{}", json!({"id": id, "method": "engine.commands"})).unwrap();
|
||||
let req = connection.requests.recv_timeout(Duration::from_secs(3)).unwrap();
|
||||
let req = connection.requests.recv_timeout(Duration::from_secs(10)).unwrap();
|
||||
assert_eq!(req.method, "engine.commands");
|
||||
assert!(req.take());
|
||||
if id == 1 {
|
||||
connection.client.get_mut().write_all(request(2).as_bytes()).unwrap();
|
||||
} else {
|
||||
connection.client.get_mut().shutdown(std::net::Shutdown::Write).unwrap();
|
||||
}
|
||||
// A dispatched command waits for its reply, not for socket input.
|
||||
std::thread::sleep(Duration::from_millis(250));
|
||||
req.reply.send(json!({"ok": true, "result": id})).unwrap();
|
||||
@@ -312,7 +321,6 @@ mod tests {
|
||||
let reply: Value = serde_json::from_str(&reply).unwrap();
|
||||
assert_eq!(reply, json!({"id": id, "ok": true, "result": id}));
|
||||
}
|
||||
connection.client.get_mut().shutdown(std::net::Shutdown::Write).unwrap();
|
||||
let mut eof = String::new();
|
||||
connection.client.read_to_string(&mut eof).unwrap();
|
||||
connection.join();
|
||||
@@ -327,7 +335,8 @@ mod tests {
|
||||
let (req, rrx) = ControlRequest::new("ui.inspect", json!({}));
|
||||
let t0 = std::time::Instant::now();
|
||||
let r = await_reply(&rrx, &req.state, || 7, || {}, Duration::from_millis(50), Duration::from_secs(10));
|
||||
assert!(t0.elapsed() < Duration::from_secs(2));
|
||||
// the frame window (50 ms), not the 10 s timeout; the margin is for a slow machine
|
||||
assert!(t0.elapsed() < Duration::from_secs(5), "{:?}", t0.elapsed());
|
||||
assert_eq!(r["ok"], false);
|
||||
assert!(r["error"].as_str().unwrap().contains("not visible"), "{r}");
|
||||
assert!(!req.take(), "the app does not run it once it shows up");
|
||||
|
||||
@@ -456,10 +456,6 @@ mod tests {
|
||||
set_language(*language);
|
||||
assert!(seen.insert(structure_of(&bar)), "{language:?} shares another language's menu structure key");
|
||||
}
|
||||
set_language(Locale::ZhHant);
|
||||
assert_eq!(label_text("File"), "檔案");
|
||||
assert_eq!(label_text("Settings…"), "設定…");
|
||||
assert_eq!(label_text("Quit LightCraft"), "結束 LightCraft");
|
||||
set_language(Locale::En);
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ jpegxl-rs / imagequant (GPL / C), dav1d / libheif (C).
|
||||
|
||||
## Performance (Apple M-series, release, synthetic 24 MP JPEG q90 4:2:0)
|
||||
|
||||
Run `cargo test --release -p lightcraft-codecs --test bench -- --ignored --nocapture`.
|
||||
Run `cargo test --release -p lightcraft-codecs --test it bench:: -- --ignored --nocapture`.
|
||||
|
||||
| Operation | Time |
|
||||
|---|---|
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Benchmark-ish, ignored by default. Run with:
|
||||
//! `cargo test --release -p lightcraft-codecs --test bench -- --ignored --nocapture`
|
||||
//! `cargo test --release -p lightcraft-codecs --test it bench:: -- --ignored --nocapture`
|
||||
|
||||
use lightcraft_codecs::*;
|
||||
use std::time::Instant;
|
||||
@@ -1,7 +1,6 @@
|
||||
//! HEIC/HEIF through the codec API (feature `heif`), on lossless synthetic files from
|
||||
//! `lightcraft_heif::testdata`: an iPhone-like grid with a Display P3 profile, rotation, EXIF, a
|
||||
//! thumbnail and an HDR gain map; `nclx`-only colour; 10-bit; and hostile input.
|
||||
#![cfg(feature = "heif")]
|
||||
|
||||
use lightcraft_codecs::*;
|
||||
use lightcraft_heif::testdata::{Prop, Spec, build};
|
||||
@@ -1,5 +1,3 @@
|
||||
#![cfg(all(feature = "jxl", not(target_arch = "wasm32")))]
|
||||
|
||||
use lightcraft_codecs::exif::minimal_exif;
|
||||
use lightcraft_codecs::*;
|
||||
use lightcraft_raster::Rgba8;
|
||||
@@ -0,0 +1,13 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
//! `tests/jxl_tail_heap.rs` stays a binary of its own: it installs dhat as the global allocator.
|
||||
|
||||
mod bench;
|
||||
mod corpus;
|
||||
mod float_tiff;
|
||||
mod fuzz;
|
||||
mod header;
|
||||
#[cfg(feature = "heif")]
|
||||
mod heif;
|
||||
#[cfg(all(feature = "jxl", not(target_arch = "wasm32")))]
|
||||
mod jxl_streaming;
|
||||
mod roundtrip;
|
||||
@@ -0,0 +1,4 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
|
||||
#[cfg(feature = "runtime")]
|
||||
mod real_model;
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! ```text
|
||||
//! LC_DENOISE_MODEL=<model_bayer.onnx> LC_DENOISE_MOSAICS=<folder> LC_DENOISE_OUT=<folder> \
|
||||
//! cargo test --release -p lightcraft-denoise --features runtime --test real_model -- --ignored --nocapture
|
||||
//! cargo test --release -p lightcraft-denoise --features runtime --test it real_model:: -- --ignored --nocapture
|
||||
//! ```
|
||||
//!
|
||||
//! `LC_DENOISE_MOSAICS` holds `<name>.mosaic.f32` (little-endian f32, normalised: black 0, white 1, not white
|
||||
@@ -11,8 +11,6 @@
|
||||
//! once, default 8), `LC_DENOISE_ONLY` (one name), `LC_DENOISE_ISO` (noise strength, default 1.0; 0 adds none),
|
||||
//! `LC_DENOISE_FULL` (the whole frame instead of a 2048 × 1536 crop).
|
||||
|
||||
#![cfg(feature = "runtime")]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -38,13 +38,13 @@ impl std::error::Error for NotFollowed {}
|
||||
/// was opened from.
|
||||
pub fn resolve(stored: &str, document: Option<&Path>) -> Result<PathBuf, NotFollowed> {
|
||||
let document = document.map(|d| if d.is_relative() { std::path::absolute(d).unwrap_or_else(|_| d.to_path_buf()) } else { d.to_path_buf() });
|
||||
let path = craft_docpath::resolve(stored, document.as_deref()).map_err(NotFollowed::Refused)?;
|
||||
// A path that is still relative would be taken from the working directory, which the
|
||||
// document does not choose.
|
||||
if path.is_relative() {
|
||||
return Err(NotFollowed::NotHere);
|
||||
match craft_docpath::resolve_absolute(stored, document.as_deref()) {
|
||||
Ok(path) => Ok(path),
|
||||
// A usable path that is not absolute here: a Windows path on macOS or Linux, or a
|
||||
// relative path with no document folder to take it from.
|
||||
Err(Refused::Invalid) if craft_docpath::resolve(stored, document.as_deref()).is_ok() => Err(NotFollowed::NotHere),
|
||||
Err(r) => Err(NotFollowed::Refused(r)),
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -59,5 +59,12 @@ mod tests {
|
||||
assert_eq!(resolve(r"\\192.0.2.1\share\x.jpg", Some(&doc)), Err(NotFollowed::Refused(Refused::Network)));
|
||||
assert_eq!(resolve("file://192.0.2.1/s/x.jpg", Some(&doc)), Err(NotFollowed::Refused(Refused::Network)));
|
||||
assert_eq!(resolve(r"\\.\COM1", Some(&doc)), Err(NotFollowed::Refused(Refused::Device)));
|
||||
#[cfg(windows)]
|
||||
assert_eq!(resolve("Photos/2024:05/a.jpg", Some(&doc)), Err(NotFollowed::Refused(Refused::Invalid)));
|
||||
#[cfg(not(windows))]
|
||||
assert_eq!(resolve(r"C:\Photos\a.jpg", Some(&doc)), Err(NotFollowed::NotHere));
|
||||
let gvfs = Path::new("/run/user/1000/gvfs/smb-share:server=tokyo,share=photos/catalog.lrcat");
|
||||
#[cfg(not(windows))]
|
||||
assert_eq!(resolve("a.jpg", Some(gvfs)), Ok(gvfs.with_file_name("a.jpg")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2809,7 +2809,7 @@ pub(crate) mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A Sony ARW of `model`: a 64 × 48 uncompressed 14-bit CFA strip in a SubIFD (as `crates/raw/tests/vendor_robust.rs`
|
||||
/// A Sony ARW of `model`: a 64 × 48 uncompressed 14-bit CFA strip in a SubIFD (as `crates/raw/tests/it/vendor_robust.rs`
|
||||
/// lays it out) whose IFD carries the full-frame 16–35 mm distortion table (tag 0x7037).
|
||||
fn sony_arw(model: &str) -> Vec<u8> {
|
||||
use lightcraft_tiff::tags::{self as t, photometric};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! AI denoise through the engine: installing a model, making a photo's denoised picture in the background and for an
|
||||
//! export, the Amount slider mixing it in, and the cache. A tiny stand-in model (it halves every sample) takes the
|
||||
//! place of a real one, which is enough to see what the machinery does with its answer; how well real models denoise is
|
||||
//! measured separately (`crates/denoise/tests/real_model.rs`).
|
||||
//! measured separately (`crates/denoise/tests/it/real_model.rs`).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
//! Opt-in same-machine denoise model-stage benchmark on one public Bayer raw.
|
||||
//! LC_DENOISE_MODEL=<onnx> LC_DENOISE_RAW=<CC0 corpus/raw> cargo test --release
|
||||
//! -p lightcraft-engine --features denoise --test denoise_cpu -- --ignored --nocapture
|
||||
//! -p lightcraft-engine --features denoise --test it denoise_cpu:: -- --ignored --nocapture
|
||||
//! LC_DENOISE_ONLY selects a file (default arw-sony-a7m3-compressed.arw).
|
||||
//! The whole model stage includes packing, scale/clip protection, blending and normalization;
|
||||
//! file decode, model loading and GPU setup are outside the timer. It uses the application's tile pool convention.
|
||||
#![cfg(feature = "denoise")]
|
||||
|
||||
use lightcraft_denoise::{
|
||||
TileRunner,
|
||||
@@ -4,12 +4,11 @@
|
||||
//!
|
||||
//! ```text
|
||||
//! LC_DENOISE_MODEL=<model_bayer.onnx> LC_DENOISE_RAW=<folder with raw files> \
|
||||
//! cargo test --release -p lightcraft-engine --features denoise --test denoise_real -- --ignored --nocapture
|
||||
//! cargo test --release -p lightcraft-engine --features denoise --test it denoise_real:: -- --ignored --nocapture
|
||||
//! ```
|
||||
//!
|
||||
//! `LC_DENOISE_RAW` is a folder of CC0 raw files (`cargo xtask corpus --download` → `corpus/raw`); every file in it is
|
||||
//! tried. Optional: `LC_DENOISE_ONLY` (a file name to try alone), `LC_DENOISE_RUN_ON=cpu|gpu|auto` (where the model runs; `cpu` to compare with the card).
|
||||
#![cfg(feature = "denoise")]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -0,0 +1,7 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
|
||||
#[cfg(feature = "denoise")]
|
||||
mod denoise_cpu;
|
||||
#[cfg(feature = "denoise")]
|
||||
mod denoise_real;
|
||||
mod preview_read;
|
||||
@@ -2,7 +2,7 @@
|
||||
//! network share, which is where reading only the header instead of the whole raw matters:
|
||||
//!
|
||||
//! LC_PREVIEW_RAW="/run/user/1000/gvfs/smb-share:.../DSCF0001.RAF" \
|
||||
//! cargo test --release -p lightcraft-engine --test preview_read -- --ignored --nocapture
|
||||
//! cargo test --release -p lightcraft-engine --test it preview_read:: -- --ignored --nocapture
|
||||
//!
|
||||
//! It reports what the preview cost and fails when the read is not the small part of the file the
|
||||
//! header path promises. Skipped without `LC_PREVIEW_RAW` (there is no committed raw fixture: media
|
||||
@@ -118,7 +118,9 @@ impl Drop for Buf {
|
||||
let mut b = Some(b);
|
||||
let _ = RETIRED.try_with(|r| r.0.borrow_mut().extend(b.take()));
|
||||
match b {
|
||||
Some(b) => std::mem::forget(b),
|
||||
// Leaking is deliberate during thread teardown — dropping a wgpu buffer whose device
|
||||
// thread-locals have already gone can abort the process. The device owns it.
|
||||
Some(b) => std::mem::forget(b), // NOSONAR
|
||||
None => {
|
||||
RETIRED_BYTES.fetch_add(size, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! What GPU this machine offers the develop pipeline, as one JSON line: the capability probe of
|
||||
//! `cargo xtask ci`, which is std + serde_json and has no wgpu of its own. Ignored in normal runs;
|
||||
//! xtask runs it after the workspace tests, so it costs no extra build:
|
||||
//! `cargo test --workspace --test gpu_probe -- --ignored --nocapture`.
|
||||
//! `cargo test --workspace --test it -- --ignored --exact gpu_probe::gpu_probe --nocapture`.
|
||||
//!
|
||||
//! - `LIGHTCRAFT_GPU_PROBE` unset: the compute device the pipeline picks (`available()`,
|
||||
//! `adapter_name()`, `unavailable_reason()`), the backends it may use and the backends this build
|
||||
@@ -11,7 +11,6 @@
|
||||
//! a driver that crashes while it is enumerated (issue #136) costs only its own line.
|
||||
//!
|
||||
//! Only reports: which adapters the pipeline accepts is `ctx.rs`'s policy.
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use lightcraft_gpu::backend::{self, Backends};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
//! Binaries of their own: `tests/shutdown.rs` and `tests/shutdown_first.rs` close the GPU for the whole
|
||||
//! process, and `tests/memory.rs` checks the process's device-memory counters, which renders on other test
|
||||
//! threads would change.
|
||||
|
||||
mod equivalence;
|
||||
mod fallback;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod gpu_probe;
|
||||
@@ -0,0 +1,3 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
|
||||
mod e2e;
|
||||
@@ -0,0 +1,5 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
|
||||
mod hdr;
|
||||
mod pano;
|
||||
mod white_balance;
|
||||
@@ -0,0 +1,3 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
|
||||
mod robust;
|
||||
@@ -0,0 +1,3 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
|
||||
mod preview_save;
|
||||
Vendored
+1
-1
@@ -373,7 +373,7 @@ fn split_dimension(length: usize, before: bool, after: bool) -> (usize, usize) {
|
||||
/// low-pass columns (bands 8 and 5, whose low-pass columns the next level transforms again) at an odd
|
||||
/// width: an even synthesis width gains one column, which synthesis does not use. Every other band,
|
||||
/// including band 2 of the last level, is coded at its synthesis width. Measured on every C-RAW plane
|
||||
/// that `tests/cr3_corpus.rs` decodes (CC0 raw.pixls.us files): two-tile files with tile widths 2816,
|
||||
/// that `tests/it/cr3_corpus.rs` decodes (CC0 raw.pixls.us files): two-tile files with tile widths 2816,
|
||||
/// 3144, 3192, 3444 and 3564 (G5 X Mark II, M50, R100, 250D, RP, R, 90D, M6 Mark II), where bands 5
|
||||
/// and 8 gain a column in left tiles exactly when the rule says so, and single-tile `0x100` and
|
||||
/// `0x200` files, which never extend a band. In every one of those planes, each band's entropy stream
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
//! Every file must be recognised, carry an embedded JPEG preview (optional for DNG, generic CFA TIFF,
|
||||
//! older Panasonic RAW and HEVC-preview CR3), and either decode to a valid image or report
|
||||
//! `Unsupported` for one of the variants we know we don't decode yet. Prints decode times
|
||||
//! (`cargo test -p lightcraft-raw --release --test corpus -- --nocapture`).
|
||||
//! (`cargo test -p lightcraft-raw --release --test it corpus:: -- --nocapture --skip _corpus::`; the
|
||||
//! `--skip` leaves out the other `*_corpus` modules).
|
||||
|
||||
use lightcraft_raw::{RawError, RawFormat, decode, embedded_preview, probe, probe_info};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -946,7 +947,7 @@ fn corpus_samsung_srw() {
|
||||
#[test]
|
||||
fn corpus_fujifilm_compressed_samples() {
|
||||
use sha2::{Digest, Sha256};
|
||||
let checksums = include_str!("../../../docs/raf-corpus.sha256");
|
||||
let checksums = include_str!("../../../../docs/raf-corpus.sha256");
|
||||
let cases: &[(&str, usize, usize, u64, u64)] = &[
|
||||
("raf-fuji-gfx100-3773.raf", 11808, 8754, 483668733090, 6453395759968151080),
|
||||
("raf-fuji-gfx100-3775.raf", 11808, 8754, 120434414850, 6144091676930008551),
|
||||
@@ -1200,7 +1201,7 @@ fn sony_pins_match_the_corpus_manifest() {
|
||||
lines.sort();
|
||||
let digest: String = Sha256::digest(lines.concat()).iter().map(|b| format!("{b:02x}")).collect();
|
||||
assert_eq!(digest, SONY_PINS_SHA256, "the pinned Sony SHA-256 table changed: check every changed pin against its file, then update the anchor");
|
||||
let manifest = include_str!("../../../xtask/src/main.rs");
|
||||
let manifest = include_str!("../../../../xtask/src/main.rs");
|
||||
for (name, sha) in SONY_SAMPLES {
|
||||
let entry = manifest.find(&format!("\"{name}\"")).unwrap_or_else(|| panic!("{name} is not in the manifest"));
|
||||
assert!(manifest[entry..].split_once("),").is_some_and(|(e, _)| e.contains(sha)), "{name}: pinned SHA-256 differs from the manifest");
|
||||
+1
-1
@@ -10,7 +10,7 @@ use lightcraft_raw::{RawData, decode, probe_info};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const RECORDS: &str = include_str!("../../../docs/raw/coverage-sweep.tsv");
|
||||
const RECORDS: &str = include_str!("../../../../docs/raw/coverage-sweep.tsv");
|
||||
|
||||
#[test]
|
||||
fn coverage_sweep_sensor_decodes_are_pinned() {
|
||||
@@ -0,0 +1,20 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
//! `tests/sr2_heap.rs` stays a binary of its own: it installs dhat as the global allocator.
|
||||
|
||||
mod body_corpus;
|
||||
mod canon_pentax_corpus;
|
||||
mod corpus;
|
||||
mod coverage_sweep_corpus;
|
||||
mod cr3_corpus;
|
||||
mod cr3_variants;
|
||||
mod crx_properties;
|
||||
mod dng;
|
||||
mod lossy_dng;
|
||||
mod nrw_be16;
|
||||
mod olympus_samsung_corpus;
|
||||
mod rendered_dng;
|
||||
mod robust;
|
||||
mod sony_leica_corpus;
|
||||
mod sony_lens_inventory;
|
||||
mod sparse_corpus;
|
||||
mod vendor_robust;
|
||||
@@ -5,7 +5,7 @@
|
||||
//! For every file the plan reaches `Ready` on, probing the partly loaded buffer must give exactly
|
||||
//! what probing the whole file gives — with the skipped bytes zero and again with them 0xFF, so a
|
||||
//! reader that looked at skipped data would be caught. CR3 and DNG files must be described from a
|
||||
//! small part of the file (`cargo test -p lightcraft-raw --release --test sparse_corpus -- --nocapture`
|
||||
//! small part of the file (`cargo test -p lightcraft-raw --release --test it sparse_corpus:: -- --nocapture`
|
||||
//! prints what each format reads).
|
||||
|
||||
use lightcraft_raw::sparse::{HeaderPlan, Ranges, header_plan, preview_plan};
|
||||
@@ -0,0 +1,4 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
|
||||
mod reference;
|
||||
mod remote;
|
||||
@@ -16,7 +16,7 @@
|
||||
//!
|
||||
//! It prints the note to stderr as before and, when the [`SKIP_LOG_ENV`] variable names a file
|
||||
//! (`cargo xtask ci` sets it), appends one JSON line there:
|
||||
//! `{"need":"gpu","reason":"no GPU adapter","test":"camera_tone","binary":"equivalence","location":"crates/gpu/tests/equivalence.rs:22"}`.
|
||||
//! `{"need":"gpu","reason":"no GPU adapter","test":"equivalence::camera_tone","binary":"it","location":"crates/gpu/tests/it/equivalence.rs:22"}`.
|
||||
//! `cargo xtask ci` lists the skips by need and reason in its summary and in
|
||||
//! `target/ci/report.json`. A test that runs part of its checks (e.g. only the CPU half) records
|
||||
//! the part it could not run the same way.
|
||||
@@ -142,13 +142,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn an_entry_is_one_json_line_with_its_text_escaped() {
|
||||
let line = entry(Need::Corpus, "skip: C:\\corpus\\raw \"absent\"\n\u{1}", "corpus::decodes", "corpus", "crates\\raw\\tests\\corpus.rs", 28);
|
||||
let line = entry(Need::Corpus, "skip: C:\\corpus\\raw \"absent\"\n\u{1}", "corpus::decodes", "it", "crates\\raw\\tests\\it\\corpus.rs", 28);
|
||||
assert!(line.ends_with('\n'));
|
||||
assert_eq!(line.matches('\n').count(), 1, "{line}");
|
||||
assert_eq!(
|
||||
line,
|
||||
"{\"need\":\"corpus\",\"reason\":\"skip: C:\\\\corpus\\\\raw \\\"absent\\\"\\n\\u0001\",\"test\":\"corpus::decodes\",\
|
||||
\"binary\":\"corpus\",\"location\":\"crates/raw/tests/corpus.rs:28\"}\n"
|
||||
\"binary\":\"it\",\"location\":\"crates/raw/tests/it/corpus.rs:28\"}\n"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
|
||||
mod roundtrip;
|
||||
+8
-16
@@ -7,9 +7,9 @@
|
||||
//! `{:+}`… keep their meaning). The English source is the fallback arm. Adding a language means
|
||||
//! adding its file (and its `language_table!` entry); this script picks the file up by name.
|
||||
//!
|
||||
//! Every catalog must carry the same messages: a `tr_format!` call whose message is in no catalog
|
||||
//! does not compile, so a message missing from one language would otherwise go unnoticed until it
|
||||
//! shows English.
|
||||
//! A message a language lacks (or translates as an empty string) shows the English source: that
|
||||
//! language has no arm for it, and a message no catalog has formats the English source through the
|
||||
//! macro's last arm. `i18n::tests::translation_gaps_report` lists the gaps per language.
|
||||
//!
|
||||
//! It also bakes the contributor credits into the binary as static Rust tables (`$OUT_DIR/credits.rs`,
|
||||
//! included by `src/credits.rs`; craftrules `standards/contributors.md`): commit stats from
|
||||
@@ -61,25 +61,14 @@ fn generate() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let Some(code) = name.strip_suffix("-formats.json").filter(|code| *code != "en") else { continue };
|
||||
println!("cargo:rerun-if-changed=locales/{name}");
|
||||
let variant = variant_for(code)?;
|
||||
let messages: BTreeMap<String, String> =
|
||||
let mut messages: BTreeMap<String, String> =
|
||||
serde_json::from_str(&fs::read_to_string(&path)?).map_err(|error| format!("locales/{name}: {error}"))?;
|
||||
messages.retain(|_, translated| !translated.is_empty());
|
||||
catalogs.push((code.to_string(), variant, messages));
|
||||
}
|
||||
catalogs.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
// Every catalog translates the same messages.
|
||||
let all: BTreeSet<&String> = catalogs.iter().flat_map(|(_, _, messages)| messages.keys()).collect();
|
||||
let mut problems = Vec::new();
|
||||
for (code, _, messages) in &catalogs {
|
||||
for key in &all {
|
||||
if !messages.contains_key(*key) {
|
||||
problems.push(format!("locales/{code}-formats.json is missing {key:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !problems.is_empty() {
|
||||
return Err(problems.join("\n").into());
|
||||
}
|
||||
|
||||
let mut source = String::from(concat!(
|
||||
"/// Translate a format string, checked by `format!` in every language.\n",
|
||||
@@ -98,6 +87,9 @@ fn generate() -> Result<String, Box<dyn std::error::Error>> {
|
||||
source.push_str(&format!(" _ => format!({en} $(, $($args)*)?),\n }}\n }};\n"));
|
||||
}
|
||||
source.push_str(concat!(
|
||||
" ($english:literal $(, $($args:tt)*)?) => {\n",
|
||||
" format!($english $(, $($args)*)?)\n",
|
||||
" };\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"// `macro_rules!` is textual: the module re-export is what lets call sites say\n",
|
||||
|
||||
@@ -2344,7 +2344,6 @@
|
||||
"Loupe Info 1": "Info lente 1",
|
||||
"Loupe Info 2": "Info lente 2",
|
||||
"Use Defaults": "Usa predefiniti",
|
||||
"Dimensions": "Dimensioni",
|
||||
"Camera & Lens": "Fotocamera e obiettivo",
|
||||
"View → View Options chooses the lines Info 1 and Info 2 show.": "Visualizza → Opzioni di visualizzazione sceglie le righe di Info 1 e Info 2."
|
||||
}
|
||||
|
||||
@@ -2495,7 +2495,7 @@ mod tests {
|
||||
assert!(!row.cancellable, "{row:?}");
|
||||
assert!(h.app.session.activity.cancel(row.id).is_err(), "Photos can't be stopped once asked");
|
||||
let current = h.app.export.as_ref().map(|e| e.progress.lock().unwrap().1.clone()).unwrap_or_default();
|
||||
assert_eq!(current, "Wird zu Apple Fotos hinzugefügt…");
|
||||
assert_eq!(current, crate::i18n::Locale::De.tr("Adding to Apple Photos…"));
|
||||
// the row says so on the next frame (issue #345)
|
||||
h.step();
|
||||
let row = h.app.session.activity.list().into_iter().find(|r| r.kind == "export").expect("the export's row");
|
||||
@@ -4570,9 +4570,6 @@ mod tests {
|
||||
assert_eq!(crate::i18n::language(), locale);
|
||||
for (mode, key) in [("basic", "Info Overlay: File"), ("exposure", "Info Overlay: Exposure"), ("off", "Info Overlay Off")] {
|
||||
let expected = locale.tr(key);
|
||||
if locale != Locale::En {
|
||||
assert_ne!(expected, key, "this locale must translate the tested message");
|
||||
}
|
||||
assert_eq!(h.request("ui.key", json!({"key": "i", "cmd": true}), t)["ok"], true);
|
||||
assert_eq!(serde_json::to_value(h.app.ui.info_overlay).unwrap(), json!(mode));
|
||||
assert_eq!(h.app.ui.toast.as_ref().expect("overlay toast").0, expected);
|
||||
|
||||
+383
-806
File diff suppressed because it is too large
Load Diff
@@ -684,25 +684,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skipped_images_are_one_line_in_the_ui_language() {
|
||||
fn skipped_images_are_one_line() {
|
||||
let value = json!({"report": {"photos": 3, "collections": 0, "warnings": ["w1", "w2"], "skipped": [
|
||||
{"image": 7, "reason": "missingFile"},
|
||||
{"image": 8, "reason": "missingRoot"},
|
||||
{"image": 9, "reason": "masterSkipped"},
|
||||
]}});
|
||||
let details = |locale| {
|
||||
crate::i18n::set_language(locale);
|
||||
let dialog = result_dialog(&value, "done");
|
||||
crate::i18n::set_language(crate::i18n::Locale::En);
|
||||
let crate::state::Dialog::LightroomResult { details, .. } = dialog else { panic!("not a Lightroom result") };
|
||||
details
|
||||
};
|
||||
let en = details(crate::i18n::Locale::En);
|
||||
crate::i18n::set_language(crate::i18n::Locale::En);
|
||||
let crate::state::Dialog::LightroomResult { details: en, .. } = result_dialog(&value, "done") else { panic!("not a Lightroom result") };
|
||||
assert_eq!(en.len(), 4, "{en:?}");
|
||||
assert_eq!(en[1], "3 catalog images were not imported because their records in the catalog are incomplete.");
|
||||
assert_eq!(&en[2..], ["w1", "w2"]);
|
||||
let es = details(crate::i18n::Locale::Es);
|
||||
assert_eq!(es[1], "No se importaron 3 imágenes del catálogo porque sus registros en el catálogo están incompletos.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1533,18 +1533,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn library_context_menu_labels_have_french_translations() {
|
||||
let saved = crate::i18n::language();
|
||||
crate::i18n::set_language(crate::i18n::Locale::Fr);
|
||||
for (label, expected) in
|
||||
[("Pick", "Sélectionnée"), ("Reject", "Rejeter"), ("Group into Stack", "Regrouper en pile"), ("HDR Panorama…", "Panorama HDR…")]
|
||||
{
|
||||
assert_eq!(crate::i18n::tr(label), expected);
|
||||
}
|
||||
crate::i18n::set_language(saved);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square_grid_groups_and_auto_levels() {
|
||||
let l = layout(&[1.0; 5], &[(0, 3), (3, 2)], 400.0, 100.0, true);
|
||||
|
||||
@@ -520,6 +520,35 @@ fn in_tree(tree: &[KeywordNode], path: &str) -> bool {
|
||||
tree.iter().any(|n| same(&n.path, path) || (lightcraft_catalog::keywords::is_under(path, &n.path) && in_tree(&n.children, path)))
|
||||
}
|
||||
|
||||
/// The labels the Keyword List and its dialogs pass to `tr` through variables, out of reach of a
|
||||
/// literal search: the translation gaps report (`i18n`) checks them.
|
||||
#[cfg(test)]
|
||||
pub(crate) const VARIABLE_LABELS: &[&str] = &[
|
||||
"Include on Export",
|
||||
"Export Containing Keywords",
|
||||
"Export Synonyms",
|
||||
"Person",
|
||||
"Merge Keywords",
|
||||
"No keywords yet",
|
||||
"No keywords match",
|
||||
"Add to Selected Photos",
|
||||
"Remove from Selected Photos",
|
||||
"Keywords",
|
||||
"& Containing",
|
||||
"Will Export",
|
||||
"No keywords are exported",
|
||||
"Add to All Selected Photos",
|
||||
"Set name",
|
||||
"Edit Set…",
|
||||
"Edit Keyword Set",
|
||||
"A keyword set needs a name",
|
||||
"Recent Keywords",
|
||||
"Keywords (⌥1–⌥9)",
|
||||
"Save as a new set",
|
||||
"Import Keywords…",
|
||||
"Export Keywords…",
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use lightcraft_catalog::{Op, Photo, Source};
|
||||
@@ -589,45 +618,4 @@ mod tests {
|
||||
let (c, ids) = library(&[&["Ärzte", "ÄRZTE"], &[]]);
|
||||
assert_eq!(Ticks::of(&c, &ids).tick("ärzte"), Tick::Some);
|
||||
}
|
||||
|
||||
/// The labels the Keyword List and its dialogs pass to `tr` through variables (out of reach of
|
||||
/// a literal search) are translated in every language.
|
||||
#[test]
|
||||
fn labels_passed_through_variables_are_translated() {
|
||||
use crate::i18n::Locale;
|
||||
let labels = [
|
||||
"Include on Export",
|
||||
"Export Containing Keywords",
|
||||
"Export Synonyms",
|
||||
"Person",
|
||||
"Merge Keywords",
|
||||
"No keywords yet",
|
||||
"No keywords match",
|
||||
"Add to Selected Photos",
|
||||
"Remove from Selected Photos",
|
||||
"Keywords",
|
||||
"& Containing",
|
||||
"Will Export",
|
||||
"No keywords are exported",
|
||||
"Add to All Selected Photos",
|
||||
"Set name",
|
||||
"Edit Set…",
|
||||
"Edit Keyword Set",
|
||||
"A keyword set needs a name",
|
||||
"Recent Keywords",
|
||||
"Keywords (⌥1–⌥9)",
|
||||
"Save as a new set",
|
||||
"Import Keywords…",
|
||||
"Export Keywords…",
|
||||
];
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("locales");
|
||||
for code in ["de", "es", "fr", "pt-br", "ru", "uk", "it", "ka", "ja", "zh-hans", "zh-hant", "pl", "cs", "sv", "fi", "id", "vi"] {
|
||||
let text = std::fs::read_to_string(dir.join(format!("{code}.json"))).unwrap();
|
||||
let catalog: std::collections::HashMap<String, String> = serde_json::from_str(&text).unwrap();
|
||||
for label in labels {
|
||||
assert!(catalog.contains_key(label), "{code}: “{label}”");
|
||||
}
|
||||
}
|
||||
assert_eq!(Locale::ALL.len(), 18, "a language added: list its file above");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -897,25 +897,6 @@ fn brush_settings(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn masking_context_actions_use_catalog_translations() {
|
||||
let saved = crate::i18n::language();
|
||||
crate::i18n::set_language(crate::i18n::Locale::Fr);
|
||||
for (english, translated) in [
|
||||
("Duplicate Mask", "Dupliquer le masque"),
|
||||
("Invert Mask", "Inverser le masque"),
|
||||
("Hide Mask", "Masquer le masque"),
|
||||
("Move Up", "Monter"),
|
||||
("Brush", "Pinceau"),
|
||||
("Luminance Range", "Plage de luminance"),
|
||||
] {
|
||||
assert_eq!(menu_label(english), translated, "{english}");
|
||||
}
|
||||
// An arbitrary user name has no catalog entry and is left unchanged.
|
||||
assert_eq!(menu_label("My custom mask"), "My custom mask");
|
||||
crate::i18n::set_language(saved);
|
||||
}
|
||||
|
||||
/// The grid keeps four 52 pt tiles when the labels fit, and gives a long (translated) label
|
||||
/// room by dropping a column rather than drawing it past its tile.
|
||||
#[test]
|
||||
|
||||
@@ -343,20 +343,12 @@ mod import_review {
|
||||
}
|
||||
}
|
||||
|
||||
/// Every shipped locale paints translated destination choices and initializes a translated new name.
|
||||
/// Every shipped locale paints its own destination choices and names a new album in its language.
|
||||
#[test]
|
||||
fn import_album_picker_localizes_choices_in_every_shipped_language() {
|
||||
use crate::i18n::Locale;
|
||||
let Review { mut h, .. } = review();
|
||||
for locale in Locale::ALL {
|
||||
for source in
|
||||
["None", "New album", "New album…", "Imported Photos", "Choose an album…", "Search albums", "No albums yet", "No albums match"]
|
||||
{
|
||||
assert!(!locale.tr(source).is_empty());
|
||||
if *locale != Locale::En {
|
||||
assert_ne!(locale.tr(source), source, "{locale:?}: {source}");
|
||||
}
|
||||
}
|
||||
let r = h.request("ui.set", json!({"language": locale.code()}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
assert!(h.settle(SETTLE));
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//! The integration tests, one module per file, linked into one test binary.
|
||||
//! `tests/shutdown.rs` stays a binary of its own: it closes the GPU for the whole process.
|
||||
|
||||
mod accessibility;
|
||||
mod docking_workflows;
|
||||
+1
-1
@@ -238,7 +238,7 @@ wait and return the result (when built with the `sam` feature; the default CLI h
|
||||
the model on the CPU from the start.
|
||||
- Hostile documents: a selection keeps at most 4 detail patches (when read and when rendered),
|
||||
each at most 1024² logits; more than 64 clicks are never sent to the model.
|
||||
- Accuracy: `crates/segment/tests/reference.rs` compares every stage against the Python
|
||||
- Accuracy: `crates/segment/tests/it/reference.rs` compares every stage against the Python
|
||||
reference (`tools/sam3_reference.py`); on Metal the backbone matches to 7e-5 relative error
|
||||
and masks to ~5e-5. f16 was tried and rejected (overflow, ~7 % faster).
|
||||
- The engine feature `sam` (on in the desktop app, off for web and CLI) gates the model; without
|
||||
|
||||
+12
-1
@@ -1,6 +1,6 @@
|
||||
# Architecture
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (`cargo xtask ci` reports the machine and skipped tests; `testkit`) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (`cargo xtask ci`: one integration-test binary per crate, doctests apart, one WASM check) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6
|
||||
|
||||
How LightCraft is built today. The longer design document (`plan/architecture.md`) is local-only
|
||||
(`plan/` is gitignored); this page is the committed, current summary. Sizes are Rust lines counted
|
||||
@@ -63,6 +63,16 @@ dispatch by id, and headless snapshots render the UI without a window.
|
||||
`assets` (every asset attributed, no Adobe formats) and the WASM build. Other workflows build and
|
||||
test FreeBSD and Windows ARM64, lint packaging, and build releases.
|
||||
|
||||
The tests run under `cargo test --workspace --lib --bins --tests`, or `cargo nextest run` with the
|
||||
same targets when `LIGHTCRAFT_NEXTEST=1` is set and cargo-nextest is installed; doctests run after
|
||||
them with `cargo test --workspace --doc`. `cargo test` is the default because the demo library's
|
||||
generated scenes are cached per process, and nextest's process per test regenerates them.
|
||||
Examples are left out of the test build (clippy `--all-targets` still
|
||||
checks them). Each crate links its integration tests into one binary, `tests/it/main.rs` with one
|
||||
module per file (`cargo test -p <crate> --test it <module>::`); a test that changes state for its
|
||||
whole process (a global allocator, closing the GPU) keeps a binary of its own in `tests/`. The WASM
|
||||
build is one `cargo check --target wasm32-unknown-unknown --keep-going` over the wasm-safe crates.
|
||||
|
||||
`ci` also probes the machine: OS, CPUs, RAM, the GPU adapters of each backend (from the ignored
|
||||
`gpu_probe` test of `lightcraft-gpu`, run after the workspace tests so it reuses their build; xtask
|
||||
has no wgpu), the corpus and `CRAFT_FONTS_DIR`. Tests
|
||||
@@ -77,5 +87,6 @@ the `lightcraft-gpu` tests once per backend with a hardware adapter (`LIGHTCRAFT
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | minor | Quality gates: one integration-test binary per crate, doctests apart, nextest on request, one WASM check |
|
||||
| 2026-10-11 | minor | Quality gates: the CI machine probe, visible skips (`testkit`) and `target/ci/report.json` |
|
||||
| 2026-10-10 | major | Created from the code (crate sizes, layers, data model, rendering, control, gates) |
|
||||
|
||||
+3
-3
@@ -39,7 +39,7 @@ decoded.
|
||||
## Variant status
|
||||
|
||||
The supported and rejected CRX coding variants. Every rejection is an explicit `RawError::Unsupported` whose
|
||||
text is part of the contract: `crates/raw/tests/cr3_variants.rs` rebuilds each variant as a synthetic CR3
|
||||
text is part of the contract: `crates/raw/tests/it/cr3_variants.rs` rebuilds each variant as a synthetic CR3
|
||||
fixture and asserts the exact string, and that header probing (`probe_info`, the import path) and full
|
||||
decoding fail with the same error, because the engine shows that reason when it falls back to the embedded
|
||||
preview. The same tests decode a control fixture at both edges of each accepted range, so a rejection can
|
||||
@@ -87,7 +87,7 @@ CMP1 bit depth. A lossless sample outside the bit depth is still corrupt data.
|
||||
Other unverified coding variants return explicit unsupported errors (the table above lists them all). Header probing validates the same coding variants and
|
||||
tile/plane layout as full decoding.
|
||||
|
||||
`crates/raw/tests/cr3_corpus.rs` checks every full-sensor sample against a
|
||||
`crates/raw/tests/it/cr3_corpus.rs` checks every full-sensor sample against a
|
||||
black-box reference, before cropping or colour processing. Public CC0 files
|
||||
are downloaded by `cargo xtask corpus --download` and kept outside version
|
||||
control. The exact inputs and reference hashes are:
|
||||
@@ -194,7 +194,7 @@ All 110 CR3 files on raw.pixls.us (2026-10-12, every one CC0, 31 folders) decode
|
||||
falls back to the preview, so none of them uses a CRX variant the decoder refuses (vertical C-RAW tiles,
|
||||
partial-subband or rounded-plane coding, multi-tile 0x200, adaptive low-frequency gains) or a CR3
|
||||
sRAW / mRAW. HEIF stills are separate `.HIF` files, not CR3. One file per body is pinned in
|
||||
`canon_cr3_bodies_decode_like_the_camera_jpeg` (`crates/raw/tests/cr3_corpus.rs`; URLs and SHA-256 in
|
||||
`canon_cr3_bodies_decode_like_the_camera_jpeg` (`crates/raw/tests/it/cr3_corpus.rs`; URLs and SHA-256 in
|
||||
`RAW_SAMPLES`, `xtask/src/main.rs`): EOS-1D X Mark III, 90D, 250D, 850D, M200, M50 Mark II, M6 Mark II,
|
||||
R, R3, R5, R6, R6 Mark II, R6 Mark III, R7, R10, RP, R50, R50 V and PowerShot G5 X Mark II, G7 X Mark III,
|
||||
SX70 HS and V1, C-RAW where the body has a CC0 C-RAW sample. No external decoder was run: the sensor
|
||||
|
||||
+2
-2
@@ -117,8 +117,8 @@ Fresh checks can be run using an already installed model and CC0 raw samples:
|
||||
|
||||
```text
|
||||
LC_DENOISE_MODEL=<model.onnx> cargo test --release -p lightcraft-gpu --lib real_model_on_the_gpu_matches_cpu -- --ignored --nocapture
|
||||
LC_DENOISE_MODEL=<model.onnx> LC_DENOISE_RAW=<corpus/raw> cargo test --release -p lightcraft-engine --features rawnind-model --test denoise_cpu -- --ignored --nocapture
|
||||
LC_DENOISE_MODEL=<model.onnx> LC_DENOISE_RAW=<corpus/raw> cargo test --release -p lightcraft-engine --features rawnind-model --test denoise_real -- --ignored --nocapture
|
||||
LC_DENOISE_MODEL=<model.onnx> LC_DENOISE_RAW=<corpus/raw> cargo test --release -p lightcraft-engine --features rawnind-model --test it denoise_cpu:: -- --ignored --nocapture
|
||||
LC_DENOISE_MODEL=<model.onnx> LC_DENOISE_RAW=<corpus/raw> cargo test --release -p lightcraft-engine --features rawnind-model --test it denoise_real:: -- --ignored --nocapture
|
||||
```
|
||||
|
||||
## Another computer's graphics card
|
||||
|
||||
@@ -161,5 +161,5 @@ before demosaicing would be needed.
|
||||
vignette gain on the X-T4's real arrays, and the X-T4 geometry against its measured free fit.
|
||||
- `crates/raw/src/vendor/raf.rs`: a synthetic RAF through header and full decode, only for validated models, and a DNG
|
||||
round trip.
|
||||
- `crates/raw/tests/corpus.rs` `fuji_embedded_lens_tables_follow_the_validated_models`: the corpus' `raf-fuji-*` files,
|
||||
- `crates/raw/tests/it/corpus.rs` `fuji_embedded_lens_tables_follow_the_validated_models`: the corpus' `raf-fuji-*` files,
|
||||
or any folder of RAFs in `LIGHTCRAFT_FUJI` (the pixls set: 129 decoded, 88 warps, 98 vignette gains).
|
||||
|
||||
@@ -229,7 +229,7 @@ passes on as `RenderResult::gpu_fallback`; an export carries it as `Exported::gp
|
||||
each file of an `app.export` batch result has `gpuFallback` when its render fell back (issue #675).
|
||||
**Reproducing a smaller GPU:** `LIGHTCRAFT_GPU_LIMITS=webgpu` (or `downlevel`) creates the device
|
||||
with 128 MiB storage bindings / 256 MiB buffers, `LIGHTCRAFT_GPU_LIMITS=<n>` with n MiB / 2n MiB.
|
||||
Tests inject failures with `lightcraft_gpu::inject_fault` (`crates/gpu/tests/fallback.rs`,
|
||||
Tests inject failures with `lightcraft_gpu::inject_fault` (`crates/gpu/tests/it/fallback.rs`,
|
||||
`export_falls_back_to_the_cpu_when_gpu_work_is_lost`). The fault is thread-local, and so are its
|
||||
consequences: a simulated fatal failure stops GPU use on the injecting thread only (`available()`
|
||||
false, `unavailable_reason()` set there), until `reset_failures()`, which undoes nothing else — a
|
||||
@@ -237,7 +237,7 @@ real failure still stops the whole process. Such a test asserts on its own rende
|
||||
`last_fallback()`, so it can run beside other tests rendering on the GPU (issue #675).
|
||||
|
||||
## Correctness: CPU oracle and equivalence tests
|
||||
`crates/gpu/tests/equivalence.rs` renders the same settings on both and compares 8-bit sRGB:
|
||||
`crates/gpu/tests/it/equivalence.rs` renders the same settings on both and compares 8-bit sRGB:
|
||||
bounds **mean |Δ| < 0.5 LSB and max |Δ| ≤ 3 LSB** per channel. Measured (Apple M4 Pro, Metal):
|
||||
**max 1 LSB, mean ≤ 0.0003 LSB** for every case — 26 settings cases (all tone/colour/effects tools,
|
||||
3 vignette styles, grain, curves, grading, B&W, WB, NR, crop/straighten/flip, lens + perspective,
|
||||
@@ -254,7 +254,7 @@ cut back after the readback. The tests skip (pass with a note) when no adapter e
|
||||
non-Apple GPU verified: all 16 tests pass, 135 compared cases. 124 differ by max 1 LSB, 10 are
|
||||
bit-identical, and one (the point colour range overlay) has max 2 LSB on 0.0002% of pixels. Worst
|
||||
mean 0.0003 LSB. That is the same picture as on Metal, inside the bounds
|
||||
(`cargo test -p lightcraft-gpu --release --test equivalence -- --nocapture`).
|
||||
(`cargo test -p lightcraft-gpu --release --test it equivalence:: -- --nocapture`).
|
||||
|
||||
Remaining differences come from f32 vs f64 coordinate math, fast-math transcendental functions on
|
||||
Metal and summation order in the box filters — all far below one 8-bit step.
|
||||
|
||||
@@ -46,8 +46,9 @@ ani anglické zdrojové řetězce. Rozložení panelů a seznamu jazyků je beze
|
||||
## Ověření a revize
|
||||
|
||||
Testy `i18n::tests` ověřují registraci příkazu, přepínání přes nabídku a řídicí kanál,
|
||||
uložení jazyka, pokrytí katalogů, placeholdery, neutrální formulace počtů, datum, čas
|
||||
a české glyfy. Překladové šablony kontroluje také Rust při sestavení.
|
||||
uložení jazyka, placeholdery, datum, čas a české glyfy. Překladové šablony kontroluje také
|
||||
Rust při sestavení. Chybějící překlady se zobrazují anglicky a vypisuje je
|
||||
`i18n::tests::translation_gaps_report`; žádný test kvůli nim neselže.
|
||||
|
||||
Při jazykové korektuře doporučujeme zkontrolovat „asistované třídění“, „sada“ pro Stack,
|
||||
„bodová barva“, „měkký nátisk“ a názvy kreativních přednastavení.
|
||||
|
||||
@@ -29,8 +29,8 @@ cargo test -p lightcraft-ui-egui i18n::tests
|
||||
LIGHTCRAFT_LANGUAGE=de lightcraft-cli snapshot --demo -o deutsch.png --size 1600x1000
|
||||
```
|
||||
|
||||
Die Tests prüfen Katalogabdeckung, Platzhalter, Sprachwechsel per Menü und Steuerkanal,
|
||||
Speicherung, Datumsformate, unveränderte Benutzernamen und gerenderte Texte sowie Umlaute
|
||||
und ß. Ein weiterer Render-Test prüft die Schaltflächenbreiten; der Importtest prüft
|
||||
Die Tests prüfen Platzhalter, Sprachwechsel per Menü und Steuerkanal, Speicherung,
|
||||
Datumsformate, unveränderte Benutzernamen und gerenderte Texte sowie Umlaute und ß. Fehlende
|
||||
Übersetzungen erscheinen auf Englisch; `i18n::tests::translation_gaps_report` listet sie auf. Ein weiterer Render-Test prüft die Schaltflächenbreiten; der Importtest prüft
|
||||
auch den Sprachwechsel nach der Hintergrundprüfung der Quellen. Akzeptierte Sprachangaben umfassen `de`, `de-DE`, `de_AT.UTF-8` und `de-CH`;
|
||||
gespeichert wird stets `de`. Weitere Hinweise stehen in [localization.md](localization.md).
|
||||
|
||||
@@ -52,8 +52,9 @@ englanniksi. Käännös ei muuta kirjastotietoja eikä englanninkielisiä lähde
|
||||
## Testaus
|
||||
|
||||
`i18n::tests` tarkistaa komennon rekisteröinnin, vaihdon valikosta ja ohjauskanavasta,
|
||||
kielen tallennuksen, luetteloiden kattavuuden, paikkamerkit, määrämuodot, päivämäärän, ajan ja
|
||||
suomen kirjaimet. Rust tarkistaa muotoilumallit käännösvaiheessa.
|
||||
kielen tallennuksen, paikkamerkit, päivämäärän, ajan ja suomen kirjaimet. Rust tarkistaa
|
||||
muotoilumallit käännösvaiheessa. Puuttuvat käännökset näkyvät englanniksi, ja
|
||||
`i18n::tests::translation_gaps_report` luettelee ne; mikään testi ei epäonnistu niiden takia.
|
||||
|
||||
Kielentarkistuksessa kannattaa katsoa erityisesti: ”Väriluokittelu” (Color Grading),
|
||||
”Valoalueet / Valkoiset” (Highlights / Whites), ”Näyttövedos” (Soft Proofing),
|
||||
|
||||
@@ -19,7 +19,7 @@ not translated. Descriptions for SAM 3 go to the model unchanged.
|
||||
## Georgian script
|
||||
|
||||
Mkhedruli has no upper and lower case: `i18n::upper_first` leaves Georgian letters alone, and the
|
||||
interface never uses Mtavruli (a test checks the whole catalog). Commands are verbal nouns. Counts
|
||||
interface never uses Mtavruli. Commands are verbal nouns. Counts
|
||||
are count-neutral, noun first (`იმპორტირებულია ფოტო: 3`); English plural endings are dropped with
|
||||
`{:.0}`. Dates read `კვირა, 20.09.2026` (`{weekday}, {day:02}.{month:02}.{year}`), months `09.2026`.
|
||||
|
||||
@@ -73,7 +73,7 @@ cargo test -p lightcraft-ui-egui i18n::tests -- --nocapture
|
||||
LIGHTCRAFT_LANGUAGE=ka lightcraft-cli snapshot --demo -o georgian.png --size 1600x1000
|
||||
```
|
||||
|
||||
The tests check catalog coverage, placeholders, switching language by menu and control channel,
|
||||
saving, the `ka` locale tags, dates, count wording, the absence of Mtavruli and that sentence case
|
||||
keeps Georgian lowercase. More notes are in
|
||||
The tests check placeholders, switching language by menu and control channel, saving, the `ka`
|
||||
locale tags, dates and that sentence case keeps Georgian lowercase. Messages Georgian lacks show in
|
||||
English; `i18n::tests::translation_gaps_report` lists them. More notes are in
|
||||
[localization.md](localization.md).
|
||||
|
||||
@@ -66,8 +66,9 @@ LIGHTCRAFT_LANGUAGE=vi lightcraft-cli snapshot --demo -o vi.png --size 1600x1000
|
||||
LIGHTCRAFT_LANGUAGE=vi lightcraft-cli snapshot --demo -o vi-narrow.png --size 1280x800
|
||||
```
|
||||
|
||||
Các test tiếng Việt bắt buộc phủ catalog union, nhãn lệnh/control/Settings/Smart Album,
|
||||
đổi ngôn ngữ, lưu `vi`, formats, ngày, dữ liệu người dùng, và glyph dấu.
|
||||
Các test tiếng Việt kiểm tra đổi ngôn ngữ, lưu `vi`, formats, ngày, dữ liệu người dùng và glyph
|
||||
dấu. Chuỗi còn thiếu hiện bằng tiếng Anh và được `i18n::tests::translation_gaps_report` liệt kê;
|
||||
không test nào thất bại vì thiếu bản dịch.
|
||||
|
||||
## Nguồn dịch (clean-room)
|
||||
|
||||
|
||||
@@ -34,5 +34,6 @@ LightCraft 的界面支持英语、简体中文、繁体中文(台湾)和日
|
||||
|
||||
`LIGHTCRAFT_LANGUAGE=zh-hans lightcraft-cli snapshot ...` 可以渲染中文界面用于检查。
|
||||
|
||||
翻译是否完整、两种目录的键与占位符是否一致、字形是否齐全,都由
|
||||
两种目录的占位符是否一致、字形是否齐全,由
|
||||
`cargo test -p lightcraft-ui-egui i18n::tests` 验证(字形检查只在指定 `CRAFT_FONTS_DIR` 时实际执行)。
|
||||
缺少的译文显示为英文,由 `i18n::tests::translation_gaps_report` 列出,不会让测试失败。
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user