Skip to main content

Test Impact Analysis for C# / .NET, Java, Android (Kotlin), and Node.js — selects only tests affected by a git diff. Polyglot and monorepo aware.

Project description

opentia — Test Impact Analysis

Analyses a git diff and selects only the tests whose execution path could have been affected by the change. Skips the full suite on every push.

No external dependencies — Python 3.8+ stdlib only.

Language support: C# / .NET, Java (Maven / Gradle), Android (Kotlin, nested Gradle modules, instrumented tests), and Node.js (Jest / Vitest / npm test scripts — single packages, npm/pnpm/lerna workspaces). Mixed-language repos are handled in a single run: changes are routed to the right ecosystem automatically.


Requirements

Tool Version Purpose
Python 3.8+ Run opentia
Git any Diff source

Installation

pip install opentia

This installs the opentia command on your PATH.


Quick start

# Analyse the last commit
opentia --base HEAD~1 --root <path-to-your-project>

# Analyse uncommitted (staged + unstaged) changes — no commit needed
opentia --unstaged --root <path-to-your-project>

# Analyse and immediately run the selected tests
opentia --base HEAD~1 --root <path-to-your-project> --run

--root is where your .sln / .csproj / pom.xml / build.gradle / package.json lives. It does not need to be the git root — opentia locates the actual git root automatically.


Usage reference

opentia [OPTIONS] [-- TEST_ARGS]

  --base REF      Git ref to diff against (e.g. HEAD~1, main, origin/main)
  --head REF      Head ref to diff from (default: HEAD)
  --root DIR      Directory containing project files (default: .)
  --lang LANG     Force one adapter: dotnet | java | node (default: auto-detect all)
  --strategy      project | convention | symbol | hybrid (default: hybrid)
  --output, -o    human | json | github-actions | azure-devops (default: human)
  --run           Execute the test command after analysis
  --unstaged      Analyse working-tree changes (staged + unstaged)
  --staged        Analyse only staged changes — useful before committing
  --              Everything after this is forwarded to the test runner

Output formats

# Human-readable (default)
opentia --base HEAD~1 --root .

# JSON — pipe into scripts or CI steps
opentia --base HEAD~1 --root . --output json

# GitHub Actions
opentia --base HEAD~1 --root . --output github-actions

# Azure DevOps
opentia --base HEAD~1 --root . --output azure-devops

JSON output fields

{
  "run_all": false,               // true = targeted selection was abandoned
  "language": "dotnet",          // dotnet | java | node
  "test_filter": "FullyQualifiedName~PricingServiceTests",
  "test_project_paths": ["...SampleApp.Services.Tests.csproj"],
  "affected_test_projects": ["SampleApp.Services.Tests"],
  "affected_test_classes": ["PricingServiceTests"],
  "test_command": "dotnet test \"...\" --filter \"...\"",
  "reason": "Analysis complete",
  "strategy_notes": []            // warnings / fallback explanations
}

When changes span multiple ecosystems in one run, the same fields are emitted merged at the top level (language: "java,node", test_command joined with &&) plus a results array containing one full per-language object each.


Node.js projects

opentia detects Jest and Vitest automatically. For workspaces (npm workspaces, pnpm-workspace.yaml, or lerna.json), each sub-package is analysed independently and the dependency graph is resolved across internal references — workspace:*, file:, and plain version ranges that match a sibling package name. Changing a shared package triggers tests in every package that depends on it.

The test command is a single npx jest (or npx vitest run) invocation run from the workspace root with a test-path-pattern filter. Packages without jest/vitest but with a test script (karma, ng test, mocha-via-script) fall back to npm test --prefix <package> — selection stays package-accurate, but those packages run their full suite.

Android projects

Android repos are handled by the Gradle adapter with Kotlin-aware analysis (Kotlin types and functions are public by default — no modifier needed for symbol matching). Per module:

  • Unit tests (src/test) run via ./gradlew :path:to:module:test --tests=...; instrumented tests (src/androidTest) are selected separately and routed to :path:to:module:connectedAndroidTest (device/emulator required).
  • Nested module references (project(":core:model")) resolve by path, and each module's command carries only its own test classes — a --tests pattern matching nothing would fail the task.
  • gradle/libs.versions.toml is workspace-level INFRA (full run); local.properties, keystores (.jks/.keystore), build/ output, and hidden tooling dirs (.github/, .claude/, …) are ignored; proguard-rules.pro scopes to its owning module.
  • Method-level narrowing only applies when every changed method in a test class is @Test-annotated; a changed helper widens to the whole class.

Mixed-language monorepos

A single run covers every ecosystem under --root. Each changed file is routed to the adapter owning its nearest build file (.csproj/.sln, pom.xml/build.gradle, package.json), so a fullstack repo — say a Maven backend with an Angular frontend — selects backend tests for .java changes and frontend specs for .ts changes in one invocation:

opentia --base HEAD~1 --root .   # all ecosystems, one combined result

The combined test_command chains each runner with &&. To restrict analysis to one ecosystem, pass --lang dotnet|java|node.

Two more monorepo behaviours worth knowing:

  • Changes outside --root are ignored (reported in strategy_notes) rather than triggering a full run of the app you pointed at.
  • Module-level build files (a leaf .csproj, a module pom.xml, a workspace package's package.json) scope through the dependency graph like any other change to that project. Only workspace-level files (.sln, root/parent pom.xml, settings.gradle, root package.json, lockfile-style global config) force a full run.

CI integration

GitHub Actions — pull request

on:
  pull_request:
    branches: [main, staging]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Test Impact Analysis
        id: tia
        run: opentia --base ${{ github.event.pull_request.base.sha }} --root . --output github-actions

      - name: Run affected tests
        if: steps.tia.outputs.has_tests == 'true'
        env:
          TEST_COMMAND: ${{ steps.tia.outputs.test_command }}
        run: bash -c "$TEST_COMMAND"

Available outputs: test_filter, run_all, has_tests, test_project_paths, test_command.

Security: pass test_command through an env: variable and run bash -c "$TEST_COMMAND", rather than interpolating ${{ … }} directly into run:. opentia shell-quotes the command components (so the string is safe to evaluate in a POSIX shell), but textual ${{ }} interpolation pastes the value into the script before the shell parses it — the env-indirection keeps a repo-controlled file or directory name from ever being re-parsed as workflow/shell syntax. The emitted command targets a POSIX shell (bash); on Windows runners, set shell: bash.

GitHub Actions — push to branch

- name: Test Impact Analysis
  id: tia
  run: opentia --base ${{ github.event.before }} --root . --output github-actions

- name: Run affected tests
  if: steps.tia.outputs.has_tests == 'true'
  env:
    TEST_COMMAND: ${{ steps.tia.outputs.test_command }}
  run: bash -c "$TEST_COMMAND"

Azure DevOps

- script: opentia --base $(System.PullRequest.TargetBranchName) --root . --output azure-devops
  displayName: Test Impact Analysis

- script: $(testCommand)
  condition: eq(variables['hasTests'], 'true')
  displayName: Run affected tests

Available variables: testFilter, runAllTests, hasTests, testProjectPaths, testCommand.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

opentia-1.2.2.tar.gz (33.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

opentia-1.2.2-py3-none-any.whl (31.0 kB view details)

Uploaded Python 3

File details

Details for the file opentia-1.2.2.tar.gz.

File metadata

  • Download URL: opentia-1.2.2.tar.gz
  • Upload date:
  • Size: 33.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for opentia-1.2.2.tar.gz
Algorithm Hash digest
SHA256 4e9bfe7fb48ef98961ed3334a8c411533efe5655d761b349a33b3ac1d0bda2d7
MD5 558fbbe5672befbfd0cea189e0f2ff00
BLAKE2b-256 bf2f83bc868b0910bd006a6d889e315cab1db29a95834df2651908df219ff7ef

See more details on using hashes here.

File details

Details for the file opentia-1.2.2-py3-none-any.whl.

File metadata

  • Download URL: opentia-1.2.2-py3-none-any.whl
  • Upload date:
  • Size: 31.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for opentia-1.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 ec1cb38ffc827873772f5301c977136ab874aa8c8b9192db53f9f42da749b772
MD5 a5c938663b7762e065895766324999ce
BLAKE2b-256 2a95cbe516baf6735d32c7fb1fe0f32d41b5d3a334abef801307752ca8ddd015

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page