diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 000000000..2de868ee6 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,114 @@ +name: ttx-diff CI/CD + +on: + push: + branches: + - main + paths: + - 'ttx_diff/**' + - '.github/workflows/python.yml' + tags: + - 'ttx-diff-v*' + pull_request: + paths: + - 'ttx_diff/**' + - '.github/workflows/python.yml' + workflow_dispatch: + +jobs: + test: + name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + python-version: ['3.10', '3.13'] + # TODO: Re-enable 3.14 once gftools updates pygit2 pin from 1.16.0 + # https://github.com/googlefonts/gftools/blob/d4c06f5d88e0a849fabf7089d80835a96dc42f30/pyproject.toml#L47-L49 + + steps: + - uses: actions/checkout@v5 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package with test dependencies + working-directory: ttx_diff + run: pip install -e ".[test]" + + - name: Run tests + working-directory: ttx_diff + run: pytest -v + + lint: + name: Lint and format check + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: Check package builds + working-directory: ttx_diff + run: pipx run build + + - name: Install package with dev dependencies + working-directory: ttx_diff + run: pip install --find-links=dist 'ttx-diff[dev]' + + - name: Check formatting with ruff + working-directory: ttx_diff + run: ruff format --check src/ tests/ + + - name: Lint with ruff + working-directory: ttx_diff + run: ruff check src/ tests/ + + build: + name: Build distribution + runs-on: ubuntu-latest + needs: [test, lint] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ttx-diff-v') + + steps: + - uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Build package + working-directory: ttx_diff + run: pipx run build + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + path: ttx_diff/dist/ + + publish-pypi: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: [build] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ttx-diff-v') + permissions: + id-token: write # Required for trusted publishing + + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 97e6cd254..122ab0b79 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,11 +1,21 @@ on: pull_request: + paths-ignore: + - 'ttx_diff/**' + - '.github/workflows/python.yml' + - 'resources/scripts/requirements.in' + - 'resources/scripts/requirements.txt' merge_group: push: branches: - main tags: - "*" + paths-ignore: + - 'ttx_diff/**' + - '.github/workflows/python.yml' + - 'resources/scripts/requirements.in' + - 'resources/scripts/requirements.txt' name: Continuous integration diff --git a/fontc_crater/src/ci.rs b/fontc_crater/src/ci.rs index 6b971de4d..6e1116aa5 100644 --- a/fontc_crater/src/ci.rs +++ b/fontc_crater/src/ci.rs @@ -209,7 +209,7 @@ fn ttx_diff_has_changes(last_run_sha: &str) -> bool { .unwrap(); std::str::from_utf8(&output.stdout) .unwrap() - .contains("ttx_diff.py") + .contains("ttx_diff/") } #[derive(Debug, Default)] diff --git a/fontc_crater/src/target.rs b/fontc_crater/src/target.rs index d541e2220..1545e0951 100644 --- a/fontc_crater/src/target.rs +++ b/fontc_crater/src/target.rs @@ -145,7 +145,7 @@ impl Target { Default::default() }; let mut cmd = format!( - "python3 resources/scripts/ttx_diff.py '{repo_url}{sha_part}#{}'", + "python3 -m ttx_diff '{repo_url}{sha_part}#{}'", rel_source_path.display() ); if self.build == BuildType::GfTools { @@ -371,7 +371,7 @@ mod tests { let hmm = target.repro_command("example.com"); assert_eq!( hmm, - "python3 resources/scripts/ttx_diff.py 'example.com?123456789a#sources/hi.glyphs'" + "python3 -m ttx_diff 'example.com?123456789a#sources/hi.glyphs'" ); } } diff --git a/fontc_crater/src/ttx_diff_runner.rs b/fontc_crater/src/ttx_diff_runner.rs index 4d31722e6..8b3a64fca 100644 --- a/fontc_crater/src/ttx_diff_runner.rs +++ b/fontc_crater/src/ttx_diff_runner.rs @@ -1,12 +1,9 @@ -use std::{ - collections::BTreeMap, - path::{Path, PathBuf}, - process::Command, -}; +use std::{collections::BTreeMap, path::PathBuf, process::Command}; use crate::{BuildType, Results, RunResult, Target, ci::ResultsCache}; -static SCRIPT_PATH: &str = "./resources/scripts/ttx_diff.py"; +// Run ttx-diff via python -m to ensure we use the venv's installed version +static TTX_DIFF_MODULE: &str = "ttx_diff"; pub(super) struct TtxContext { pub fontc_path: PathBuf, @@ -24,13 +21,20 @@ pub(super) fn run_ttx_diff(ctx: &TtxContext, target: &Target) -> RunResult f32 { if val.is_nan() { 0.0 } else { val } } -/// make sure we can find and execute ttx_diff script +/// make sure we can find and execute ttx-diff module pub(super) fn assert_can_run_script() { // first check that we can find timeout(1) (not present on macOS by default, // install via homebrew) assert_has_timeout_coreutil(); - // then check that we can run the ttx_diff script itself - let path = Path::new(SCRIPT_PATH); - if !path.exists() { - eprintln!( - "cannot find script at {}", - path.canonicalize().as_deref().unwrap_or(path).display() - ); - std::process::exit(1); - } + // then check that we can run ttx-diff via python -m match Command::new("python3") - .arg(SCRIPT_PATH) - .arg("--only_check_args") + .args(["-m", TTX_DIFF_MODULE, "--only_check_args"]) .output() { Ok(output) if output.status.success() => return, Ok(output) => { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); - eprintln!("could not run ttx_diff.py. Have you setup your venv?"); + eprintln!("could not run ttx-diff. Have you setup your venv?"); if !stdout.is_empty() { eprintln!("stdout: {stdout}"); } diff --git a/resources/scripts/requirements.in b/resources/scripts/requirements.in index a07f32c0b..6d3345686 100644 --- a/resources/scripts/requirements.in +++ b/resources/scripts/requirements.in @@ -1,13 +1,10 @@ -absl-py -# keep fontmake version pinned to ensure output from ttx_diff.py is stable; +# ttx-diff package with all its dependencies (single source of truth) +-e file:./ttx_diff + +# Pin versions for CI stability (overrides ttx-diff's minimum versions) +# keep fontmake version pinned to ensure output from ttx-diff is stable; # the 'repacker' option enables faster GSUB/GPOS serialization via uharfbuzz fontmake[repacker]==3.11.0 -# technically fonttools is in turn a dependency of fontmake but a few of -# our scripts import it directly, so we list it among the top-level requirements. -fonttools -lxml -cdifflib -glyphsLib -# keep gftools pinned as well to ensure ttx_diff.py output is stable. +# keep gftools pinned as well to ensure ttx-diff output is stable. # 0.9.74 is when experimental support for fontc was added to gftools. gftools==0.9.93 diff --git a/resources/scripts/requirements.txt b/resources/scripts/requirements.txt index 50476be07..64f5081f3 100644 --- a/resources/scripts/requirements.txt +++ b/resources/scripts/requirements.txt @@ -4,12 +4,14 @@ # # pip-compile resources/scripts/requirements.in # +-e file:./ttx_diff + # via -r resources/scripts/requirements.in absl-py==2.3.1 # via - # -r resources/scripts/requirements.in # gftools # nanoemoji # picosvg + # ttx-diff afdko==4.0.2 # via gftools attrs==25.4.0 @@ -41,7 +43,7 @@ cattrs==25.3.0 # statmake # ufolib2 cdifflib==1.2.9 - # via -r resources/scripts/requirements.in + # via ttx-diff certifi==2025.10.5 # via requests cffi==2.0.0 @@ -76,6 +78,7 @@ fontmake[json,repacker]==3.11.0 # via # -r resources/scripts/requirements.in # gftools + # ttx-diff fontmath==0.9.4 # via # afdko @@ -90,7 +93,6 @@ fontpens==0.2.4 # via defcon fonttools[lxml,repacker,ufo,unicode,woff]==4.60.1 # via - # -r resources/scripts/requirements.in # afdko # axisregistry # babelfont @@ -111,6 +113,7 @@ fonttools[lxml,repacker,ufo,unicode,woff]==4.60.1 # mutatormath # nanoemoji # statmake + # ttx-diff # ufo2ft # ufolib2 # ufomerge @@ -126,7 +129,9 @@ gflanguages==0.7.7 gfsubsets==2024.9.25 # via gftools gftools==0.9.93 - # via -r resources/scripts/requirements.in + # via + # -r resources/scripts/requirements.in + # ttx-diff gitdb==4.0.12 # via gitpython gitpython==3.1.45 @@ -135,11 +140,11 @@ glyphsets==1.1.0 # via gftools glyphslib==6.12.1 # via - # -r resources/scripts/requirements.in # bumpfontversion # fontmake # gftools # glyphsets + # ttx-diff idna==3.11 # via requests importlib-resources==6.5.2 @@ -148,12 +153,12 @@ jinja2==3.1.6 # via gftools lxml==6.0.2 # via - # -r resources/scripts/requirements.in # afdko # fontfeatures # fonttools # nanoemoji # picosvg + # ttx-diff markdown-it-py==4.0.0 # via rich markupsafe==3.0.3 @@ -177,7 +182,7 @@ openstep-plist==0.5.1 # glyphslib opentype-sanitizer==9.2.0 # via gftools -orjson==3.11.3 +orjson==3.11.4 # via # babelfont # ufolib2 @@ -220,6 +225,7 @@ pyyaml==6.0.3 # via # gftools # glyphsets + # ttx-diff regex==2025.10.23 # via nanoemoji requests==2.32.5 @@ -232,7 +238,7 @@ resvg-cli==0.44.0 # via nanoemoji rich==14.2.0 # via gftools -ruamel-yaml==0.18.15 +ruamel-yaml==0.18.16 # via gftools ruamel-yaml-clib==0.2.14 # via ruamel-yaml diff --git a/ttx_diff/.gitignore b/ttx_diff/.gitignore new file mode 100644 index 000000000..007dda451 --- /dev/null +++ b/ttx_diff/.gitignore @@ -0,0 +1,33 @@ +# Python cache +__pycache__/ + +# Distribution / packaging +build/ +dist/ +*.egg-info/ + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +coverage.xml +*.cover +.pytest_cache/ + +# Environments +.venv +venv/ + +# OSX Finder +.DS_Store + +# VSCode +.vscode + +# setuptools_scm generated version file +src/ttx_diff/_version.py + +# Build artifacts +dist/ +*.egg-info/ diff --git a/ttx_diff/README.md b/ttx_diff/README.md new file mode 100644 index 000000000..3398bda76 --- /dev/null +++ b/ttx_diff/README.md @@ -0,0 +1,103 @@ +# ttx-diff + +[![PyPI](https://img.shields.io/pypi/v/ttx-diff)](https://pypi.org/project/ttx-diff/) +[![Python Version](https://img.shields.io/pypi/pyversions/ttx-diff)](https://pypi.org/project/ttx-diff/) + +A tool for comparing font compiler outputs between fontc (Rust) and fontmake (Python). + +## Overview + +`ttx-diff` is a helper utility that compares binary font outputs from two different font compilers: +- **fontc**: The Rust-based font compiler from Google Fonts +- **fontmake**: The Python-based font compiler + +The tool converts each binary font to TTX (XML) format, normalizes expected differences, and provides a detailed comparison summary. + +## Installation + +### From PyPI + +```bash +pip install ttx-diff +``` + +### From source + +```bash +git clone https://github.com/googlefonts/fontc.git +cd fontc/ttx_diff +pip install -e . +``` + +## Requirements + +- Python 3.10 or higher +- `fontc` and `otl-normalizer` binaries (see below) + +All Python dependencies (fontmake, fonttools, etc.) are installed automatically. + +### Getting fontc and otl-normalizer + +The tool needs the `fontc` and `otl-normalizer` binaries. You can: + +1. **Specify paths explicitly** (recommended for most users): + ```bash + ttx-diff --fontc_path /path/to/fontc --normalizer_path /path/to/otl-normalizer source.glyphs + ``` + +2. **Add them to your PATH**: If `fontc` and `otl-normalizer` are in your PATH, they'll be found automatically + +3. **Run from fontc repository**: If you run from the fontc repository root, the tool will automatically build the binaries for you + +## Usage + +**Note**: Unlike the original `ttx_diff.py` script, this standalone version can be run from any directory. You don't need to be in the fontc repository. + +Rebuild with both fontmake and fontc and compare: + +```bash +ttx-diff --fontc_path /path/to/fontc --normalizer_path /path/to/otl-normalizer path/to/source.glyphs +``` + +If the binaries are in your PATH: + +```bash +ttx-diff path/to/source.glyphs +``` + +Rebuild only fontc's font and reuse existing fontmake output: + +```bash +ttx-diff --rebuild fontc path/to/source.glyphs +``` + +Output results in machine-readable JSON format, as used by the [`fontc_crater`](https://github.com/googlefonts/fontc/tree/main/fontc_crater) tool. + +```bash +ttx-diff --json path/to/source.glyphs +``` + +Compare using gftools build pipeline: + +```bash +ttx-diff --compare gftools --config config.yaml path/to/source.glyphs +``` + +## Development + +Running tests + +```bash +pip install -e .[test] +pytest +``` + +Running tests with coverage + +```bash +pytest --cov=ttx_diff --cov-report=html +``` + +## Releasing + +See . diff --git a/ttx_diff/pyproject.toml b/ttx_diff/pyproject.toml new file mode 100644 index 000000000..f5b2ce543 --- /dev/null +++ b/ttx_diff/pyproject.toml @@ -0,0 +1,97 @@ +[build-system] +requires = ["setuptools>=61.0", "setuptools_scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "ttx-diff" +dynamic = ["version"] +description = "A tool for comparing font compiler outputs (fontc vs fontmake)" +readme = "README.md" +requires-python = ">=3.10" +license = "Apache-2.0" +authors = [ + {name = "Google Fonts", email = "fonts@google.com"} +] +keywords = ["fonts", "font-compilation", "fontc", "fontmake", "testing"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Testing", + "Topic :: Text Processing :: Fonts", +] +dependencies = [ + "absl-py", + "fontmake[repacker]>=3.11.0", + "fonttools", + "lxml", + "cdifflib", + "glyphsLib", + "gftools>=0.9.74", + "pyyaml", +] + +[project.optional-dependencies] +test = [ + "pytest>=7.0", + "pytest-cov", +] +dev = [ + "pytest>=7.0", + "pytest-cov", + "ruff", +] + +[project.urls] +Homepage = "https://github.com/googlefonts/fontc" +Repository = "https://github.com/googlefonts/fontc" +Issues = "https://github.com/googlefonts/fontc/issues" + +[project.scripts] +ttx-diff = "ttx_diff.cli:main" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.setuptools_scm] +# Support custom tag format for monorepo: ttx-diff-v#.#.# +tag_regex = "^ttx-diff-v(?P[^\\+]+)(?P.*)?$" +# Git repo is in parent directory (monorepo setup) +root = ".." +# Write version file relative to pyproject.toml location (not relative to root) +version_file = "src/ttx_diff/_version.py" +# Only consider ttx-diff-v* tags, ignore other tags in monorepo +# Based on DEFAULT_DESCRIBE from setuptools_scm.git, just with custom --match pattern +# https://github.com/pypa/setuptools-scm/blob/e56b78f/src/setuptools_scm/git.py#L44-L53 +git_describe_command = "git describe --dirty --tags --long --match 'ttx-diff-v*' --abbrev=40" +# Fallback version when no matching tags exist +fallback_version = "0.0.0" + +[tool.ruff] +line-length = 88 +target-version = "py310" +extend-exclude = ["src/ttx_diff/_version.py"] + +[tool.ruff.lint] +# Ruff's recommended set: pyflakes (F) + pycodestyle (E, W) + isort (I) +select = ["E", "F", "W", "I"] +ignore = [ + "E501", # line-length violations (handled by formatter) +] + +[tool.ruff.lint.per-file-ignores] +"_version.py" = ["ALL"] # Don't lint auto-generated files + +[tool.ruff.lint.isort] +known-first-party = ["ttx_diff"] diff --git a/ttx_diff/src/ttx_diff/__init__.py b/ttx_diff/src/ttx_diff/__init__.py new file mode 100644 index 000000000..c96add006 --- /dev/null +++ b/ttx_diff/src/ttx_diff/__init__.py @@ -0,0 +1,13 @@ +"""ttx-diff: A tool for comparing font compiler outputs.""" + +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("ttx-diff") +except PackageNotFoundError: + # Package is not installed, likely running from source + __version__ = "0.0.0+unknown" + +from ttx_diff.core import main + +__all__ = ["main", "__version__"] diff --git a/ttx_diff/src/ttx_diff/__main__.py b/ttx_diff/src/ttx_diff/__main__.py new file mode 100644 index 000000000..5e94d6a1e --- /dev/null +++ b/ttx_diff/src/ttx_diff/__main__.py @@ -0,0 +1,6 @@ +"""Allow running ttx-diff as `python -m ttx_diff`.""" + +from ttx_diff.cli import main + +if __name__ == "__main__": + main() diff --git a/ttx_diff/src/ttx_diff/cli.py b/ttx_diff/src/ttx_diff/cli.py new file mode 100644 index 000000000..f312af20e --- /dev/null +++ b/ttx_diff/src/ttx_diff/cli.py @@ -0,0 +1,14 @@ +"""Command-line interface for ttx-diff.""" + +from absl import app + +from ttx_diff.core import main as core_main + + +def main(): + """Entry point for the ttx-diff command-line tool.""" + app.run(core_main) + + +if __name__ == "__main__": + main() diff --git a/resources/scripts/ttx_diff.py b/ttx_diff/src/ttx_diff/core.py similarity index 95% rename from resources/scripts/ttx_diff.py rename to ttx_diff/src/ttx_diff/core.py index f5d551937..acb909e60 100755 --- a/resources/scripts/ttx_diff.py +++ b/ttx_diff/src/ttx_diff/core.py @@ -34,30 +34,29 @@ is the command that was used to run that compiler. """ -from collections import defaultdict -from absl import app -from absl import flags -from functools import cache -from lxml import etree -from pathlib import Path import json +import os import shutil import subprocess import sys -import os -import yaml -from urllib.parse import urlparse -from cdifflib import CSequenceMatcher as SequenceMatcher +import time +from collections import defaultdict from contextlib import contextmanager +from functools import cache +from pathlib import Path from tempfile import NamedTemporaryFile from typing import Any, Dict, Generator, List, Optional, Sequence, Tuple -from glyphsLib import GSFont +from urllib.parse import urlparse + +import yaml +from absl import flags +from cdifflib import CSequenceMatcher as SequenceMatcher from fontTools.designspaceLib import DesignSpaceDocument -from fontTools.varLib.iup import iup_delta -from fontTools.ttLib import TTFont from fontTools.misc.fixedTools import otRound -import time - +from fontTools.ttLib import TTFont +from fontTools.varLib.iup import iup_delta +from glyphsLib import GSFont +from lxml import etree _COMPARE_DEFAULTS = "default" _COMPARE_GFTOOLS = "gftools" @@ -168,7 +167,6 @@ def rel_user(fragment: Any) -> str: # All additional kwargs are passed to subprocess.run def log_and_run(cmd: Sequence, cwd=None, **kwargs): # Convert to ~ format because it's annoying to see really long usr paths - cmd_string = " ".join(str(c) for c in cmd) log_cmd = " ".join(rel_user(c) for c in cmd) if cwd is not None: eprint(f" (cd {rel_user(cwd)} && {log_cmd})") @@ -1351,14 +1349,49 @@ def get_fontc_and_normalizer_binary_paths(root_dir: Path) -> Tuple[Path, Path]: return (fontc_path, norm_path) -def get_crate_path(cli_arg: Optional[str], root_dir: Path, crate_name: str) -> Path: - if cli_arg: - return Path(cli_arg) +def get_crate_path( + bin_path: Optional[str], root_dir: Optional[Path], crate_name: str +) -> Path: + """Get path to a crate binary, building it if in fontc repo, or finding in PATH. + + Args: + bin_path: Path provided via CLI flag + root_dir: Path to fontc repository root (if we're in one) + crate_name: Name of the crate (e.g., "fontc" or "otl-normalizer") + + Returns: + Path to the binary - manifest_path = root_dir / crate_name / "Cargo.toml" - bin_path = root_dir / "target" / "release" / crate_name - build_crate(manifest_path) - return bin_path + Raises: + SystemExit: If binary cannot be found or built + """ + if bin_path: + path = Path(bin_path) + if not path.is_file(): + sys.exit(f"Specified {crate_name} path '{path}' does not exist") + return path + + # If we're in the fontc repo, try to build it + if root_dir is not None: + manifest_path = root_dir / crate_name / "Cargo.toml" + if manifest_path.is_file(): + built_path = root_dir / "target" / "release" / crate_name + build_crate(manifest_path) + if built_path.is_file(): + return built_path + + # Try to find in PATH + which_result = shutil.which(crate_name) + if which_result: + return Path(which_result) + + # Give helpful error message + sys.exit( + f"Could not find '{crate_name}' binary. Please either:\n" + f" 1. Specify the path with --{crate_name}_path flag\n" + f" 2. Install {crate_name} and ensure it's in your PATH\n" + f" 3. Run from the fontc repository root to build it automatically" + ) def main(argv): @@ -1367,28 +1400,35 @@ def main(argv): source = resolve_source(argv[1]).resolve() - root = Path(".").resolve() - if not (root / "fontc" / "Cargo.toml").is_file(): - sys.exit( - "This script must be run from the root of the fontc repository; " - "could not find 'fontc/Cargo.toml'." - ) - - fontc_bin_path = get_crate_path(FLAGS.fontc_path, root, "fontc") - otl_bin_path = get_crate_path(FLAGS.normalizer_path, root, "otl-normalizer") - - assert fontc_bin_path.is_file(), f"fontc path '{fontc_bin_path}' does not exist" - assert otl_bin_path.is_file(), f"normalizer path '{otl_bin_path}' does not exist" + # Check if we're in the fontc repository (optional - allows building binaries) + cwd = Path(".").resolve() + fontc_repo_root = None + if (cwd / "fontc" / "Cargo.toml").is_file(): + fontc_repo_root = cwd + eprint(f"Detected fontc repository at {rel_user(fontc_repo_root)}") + + # Get binary paths - will look in PATH or build if in repo + fontc_bin_path = get_crate_path(FLAGS.fontc_path, fontc_repo_root, "fontc") + otl_bin_path = get_crate_path( + FLAGS.normalizer_path, fontc_repo_root, "otl-normalizer" + ) if shutil.which("fontmake") is None: sys.exit("No fontmake") if shutil.which("ttx") is None: sys.exit("No ttx") - out_dir = root / "build" if FLAGS.outdir is not None: out_dir = Path(FLAGS.outdir).resolve() - assert out_dir.exists(), f"output directory {out_dir} does not exist" + if not out_dir.exists(): + sys.exit(f"Specified output directory {out_dir} does not exist") + elif fontc_repo_root is not None: + # If in fontc repo, use repo's build directory + out_dir = fontc_repo_root / "build" + else: + # Otherwise use current directory + out_dir = cwd / "ttx_diff_output" + eprint(f"No --outdir specified, using {rel_user(out_dir)}") diffs = False @@ -1445,7 +1485,3 @@ def main(argv): print_json(output) sys.exit(diffs * 2) # 0 or 2 - - -if __name__ == "__main__": - app.run(main) diff --git a/ttx_diff/tests/test_cli.py b/ttx_diff/tests/test_cli.py new file mode 100644 index 000000000..2dc1a2d5c --- /dev/null +++ b/ttx_diff/tests/test_cli.py @@ -0,0 +1,22 @@ +"""Tests for the ttx-diff CLI.""" + +import subprocess +import sys + + +def test_version(): + import ttx_diff + + assert hasattr(ttx_diff, "__version__") + assert isinstance(ttx_diff.__version__, str) + + +def test_cli_missing_source(): + """Test CLI with non-existent source file.""" + result = subprocess.run( + [sys.executable, "-m", "ttx_diff.cli", "/nonexistent/file.glyphs"], + capture_output=True, + text=True, + ) + assert result.returncode != 0 + assert "No such source" in result.stderr