diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 76515c7..a7b58cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,15 +11,6 @@ on: permissions: contents: read -env: - # Pin the required compiler so release artifacts moving with V development - # cannot destabilize the supported lane. - V_VERSION: 0.5.2 - V3_COMMIT: efae23e85b3772a28e62884d554c1e88f91f52f4 - V3_VC_COMMIT: 2cbaf6bc2367937cd5f0b4ef923d1ef9aad613b3 - V3_TCC_COMMIT: d6e7ac1b1bcc98aed734a6ecbfa8509f24606c74 - VULKAN_REF: v3.2.2 - GLFW_REF: v2.0.1 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -30,17 +21,25 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Validate and resolve tested compatibility + id: compat + shell: bash + working-directory: ${{ github.workspace }} + run: | + python ./scripts/test_compatibility.py + python ./scripts/test_doctor.py + python ./scripts/compatibility.py --github-output "$GITHUB_OUTPUT" - name: Check out pinned Vulkan module uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/vulkan - ref: ${{ env.VULKAN_REF }} + ref: ${{ steps.compat.outputs.module_vulkan }} path: .ci-modules/vulkan - name: Check out pinned GLFW module uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/glfw - ref: ${{ env.GLFW_REF }} + ref: ${{ steps.compat.outputs.module_glfw }} path: .ci-modules/glfw - name: Validate generator provenance marker run: | @@ -48,7 +47,7 @@ jobs: [[ "$generator_commit" =~ ^[0-9a-f]{40}$ ]] - uses: prantlf/setup-v-action@92f20bea50d2c9931a28c96bb4997a3c7357baf0 # v4 with: - version: ${{ env.V_VERSION }} + version: ${{ steps.compat.outputs.v_release }} - name: Install native dependencies run: | sudo apt-get update @@ -61,8 +60,8 @@ jobs: install -m 0644 opencl.v convenience.v ownership.v program.v event.v image.v svm.v capabilities.v external_interop.v v.mod VERSION LICENSE GENERATOR_COMMIT "$HOME/.vmodules/antono2/opencl/" install -d "$HOME/.vmodules/antono2/opencl/include/CL" install -m 0644 include/CL/opencl.h "$HOME/.vmodules/antono2/opencl/include/CL/opencl.h" - test "$(git -C .ci-modules/vulkan describe --tags --exact-match)" = "$VULKAN_REF" - test "$(git -C .ci-modules/glfw describe --tags --exact-match)" = "$GLFW_REF" + test "$(git -C .ci-modules/vulkan describe --tags --exact-match)" = '${{ steps.compat.outputs.module_vulkan }}' + test "$(git -C .ci-modules/glfw describe --tags --exact-match)" = '${{ steps.compat.outputs.module_glfw }}' install -d "$HOME/.vmodules/antono2/vulkan" "$HOME/.vmodules/antono2/glfw" cp -a .ci-modules/vulkan/. "$HOME/.vmodules/antono2/vulkan/" cp -a .ci-modules/glfw/. "$HOME/.vmodules/antono2/glfw/" @@ -98,9 +97,17 @@ jobs: runs-on: macos-14 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Validate and resolve tested compatibility + id: compat + shell: bash + working-directory: ${{ github.workspace }} + run: | + python ./scripts/test_compatibility.py + python ./scripts/test_doctor.py + python ./scripts/compatibility.py --github-output "$GITHUB_OUTPUT" - uses: prantlf/setup-v-action@92f20bea50d2c9931a28c96bb4997a3c7357baf0 # v4 with: - version: ${{ env.V_VERSION }} + version: ${{ steps.compat.outputs.v_release }} - name: Assemble namespaced module run: | install -d "$HOME/.vmodules/antono2/opencl" @@ -119,6 +126,14 @@ jobs: # V master currently builds a separate v1_fallback executable for tool # commands such as `v test`. Keep the complete source build together; # setup-v-action@v4's reduced cache bundle omits that executable. + - name: Validate and resolve tested compatibility + id: compat + shell: bash + working-directory: ${{ github.workspace }} + run: | + python ./scripts/test_compatibility.py + python ./scripts/test_doctor.py + python ./scripts/compatibility.py --github-output "$GITHUB_OUTPUT" - name: Check out V master uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 id: checkout_v_master @@ -162,25 +177,33 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Validate and resolve tested compatibility + id: compat + shell: bash + working-directory: ${{ github.workspace }} + run: | + python ./scripts/test_compatibility.py + python ./scripts/test_doctor.py + python ./scripts/compatibility.py --github-output "$GITHUB_OUTPUT" - name: Check out pinned V3 compiler uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: vlang/v - ref: ${{ env.V3_COMMIT }} + ref: ${{ steps.compat.outputs.v3_v }} path: .ci-v3 fetch-depth: 1 - name: Check out matching V3 bootstrap source uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: vlang/vc - ref: ${{ env.V3_VC_COMMIT }} + ref: ${{ steps.compat.outputs.v3_vc }} path: .ci-v3/vc fetch-depth: 1 - name: Check out matching V3 TinyCC runtime uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: vlang/tccbin - ref: ${{ env.V3_TCC_COMMIT }} + ref: ${{ steps.compat.outputs.v3_tcc }} path: .ci-v3/thirdparty/tcc fetch-depth: 1 - name: Build pinned V3 compiler @@ -211,9 +234,17 @@ jobs: runs-on: windows-2022 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Validate and resolve tested compatibility + id: compat + shell: bash + working-directory: ${{ github.workspace }} + run: | + python ./scripts/test_compatibility.py + python ./scripts/test_doctor.py + python ./scripts/compatibility.py --github-output "$GITHUB_OUTPUT" - uses: prantlf/setup-v-action@92f20bea50d2c9931a28c96bb4997a3c7357baf0 # v4 with: - version: ${{ env.V_VERSION }} + version: ${{ steps.compat.outputs.v_release }} - name: Install OpenCL SDK run: vcpkg install opencl:x64-windows - name: Assemble namespaced module and compile ABI probe @@ -242,6 +273,14 @@ jobs: GLFW_LIB: /usr/lib/x86_64-linux-gnu steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Validate and resolve tested compatibility + id: compat + shell: bash + working-directory: ${{ github.workspace }} + run: | + python ./scripts/test_compatibility.py + python ./scripts/test_doctor.py + python ./scripts/compatibility.py --github-output "$GITHUB_OUTPUT" - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: repository: antono2/vulkan @@ -254,7 +293,7 @@ jobs: path: .ci-modules/glfw - uses: prantlf/setup-v-action@92f20bea50d2c9931a28c96bb4997a3c7357baf0 # v4 with: - version: ${{ env.V_VERSION }} + version: ${{ steps.compat.outputs.v_release }} - name: Install native dependencies run: sudo apt-get update && sudo apt-get install -y ocl-icd-opencl-dev pocl-opencl-icd libvulkan-dev libvulkan-volk-dev libglfw3-dev - name: Assemble modules and compile particles @@ -295,6 +334,14 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: fetch-depth: 0 + - name: Validate and resolve tested compatibility + id: compat + shell: bash + working-directory: ${{ github.workspace }} + run: | + python ./scripts/test_compatibility.py + python ./scripts/test_doctor.py + python ./scripts/compatibility.py --github-output "$GITHUB_OUTPUT" - name: Validate release version and generator provenance run: | set -euo pipefail diff --git a/README.md b/README.md index e4a9549..ab92958 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ OpenCL bindings for the [V programming language](https://vlang.io/), with the complete OpenCL 1.0 through 3.0 core API, selected Khronos extensions and optional helpers for device discovery and explicit resource ownership. -[Available on VPM](https://vpm.vlang.io/packages/antono2.opencl) · -[Release notes](https://github.com/antono2/opencl/releases) · +[Available on VPM](https://vpm.vlang.io/packages/antono2.opencl) · +[Release notes](https://github.com/antono2/opencl/releases) · [Project portfolio](https://oreskin.de/projects_en.php) ## Install and setup @@ -326,3 +326,9 @@ Handwritten discovery, ownership, event, image and interoperability files have purpose comments describing their role. Make shared source and example changes in that repository, then run its publisher; direct edits here can be overwritten. Keep vendored OpenCL headers and their upstream license notices intact. + + +## Compatibility and diagnostics + +See [tested profiles and read-only diagnostics](docs/compatibility.md) for +compiler/dependency versions, setup recovery actions and upgrade checks. diff --git a/compatibility.json b/compatibility.json new file mode 100644 index 0000000..5e1489d --- /dev/null +++ b/compatibility.json @@ -0,0 +1,15 @@ +{ + "schema_version": 1, + "v_release": "0.5.2", + "v3_v": "efae23e85b3772a28e62884d554c1e88f91f52f4", + "v3_vc": "2cbaf6bc2367937cd5f0b4ef923d1ef9aad613b3", + "v3_tcc": "d6e7ac1b1bcc98aed734a6ecbfa8509f24606c74", + "modules": { + "vulkan": "v3.2.2", + "glfw": "v2.0.1" + }, + "build_tools": [ + "git", + "cmake" + ] +} diff --git a/docs/compatibility.md b/docs/compatibility.md new file mode 100644 index 0000000..eaab86a --- /dev/null +++ b/docs/compatibility.md @@ -0,0 +1,23 @@ +# Tested compatibility and diagnostics + +`compatibility.json` records the existing tested compiler, bootstrap, native and +module versions. CI resolves these values before dependency checkout and rejects +disagreement with `v.mod`. Advisory upstream-master lanes remain advisory. +Registry/version files owned by binding generators remain authoritative for +generated API contents; upstream update workflows deliberately select candidates +separately from the tested compiler profiles. + +Upgrade the manifest and affected VPM declarations together. Run +`python scripts/test_compatibility.py` and the full project CI. Consumer application +compilation/runtime checks are also required before publishing a dependency upgrade. +The resolver's `--modules PATH` option verifies installed Git tags without fetching. + +Run `python scripts/doctor.py --v PATH_TO_V --modules PATH_TO_MODULES` for read-only +compiler/tool/dependency diagnostics. Add `--json` for a support report and +`--runtime` for Vulkan device enumeration when Vulkan tools are available. Missing +or mismatched prerequisites include a recovery action; diagnostics never repair +caches or change the environment. Enumeration alone does not prove hardware video +decoding or OpenCL interoperability. Python 3.9 or newer is required for these tools. + +Choose `--profile v3` for the pinned V3 compiler or `--profile release` for +the released compiler when the project defines multiple profiles. diff --git a/scripts/compatibility.py b/scripts/compatibility.py new file mode 100644 index 0000000..4e33944 --- /dev/null +++ b/scripts/compatibility.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Resolve tested compiler/native/module profiles and validate VPM declarations.""" +import argparse +import json +from pathlib import Path +import re +import subprocess + + +def resolve(root, profile='stable'): + pins = json.loads((root / 'compatibility.json').read_text()) + if pins.get('schema_version') != 1: + raise ValueError('Unsupported compatibility schema_version') + if 'v_release' in pins and not re.fullmatch(r'[0-9]+\.[0-9]+\.[0-9]+', pins['v_release']): + raise ValueError('v_release must be a released compiler version') + upstream = root / 'third_party/upstream.json' + if upstream.is_file() and pins.get('glfw') != json.loads(upstream.read_text())['version']: + raise ValueError('Native GLFW version and compatibility.json disagree') + for key, value in pins.items(): + if key in ['v', 'vc', 'tcc', 'volk', 'generator'] or key.endswith(('_v', '_vc', '_tcc')): + if not re.fullmatch('[0-9a-f]{40}', value): + raise ValueError(f'{key} must be an immutable Git commit') + declared = dict(re.findall(r"'antono2\.([a-z0-9_]+)@([^']+)'", (root / 'v.mod').read_text())) + for name, tag in declared.items(): + if pins.get('modules', {}).get(name) != tag: + raise ValueError(f'v.mod and compatibility.json disagree for {name}') + outputs = {key: value for key, value in pins.items() if isinstance(value, str)} + for name, tag in pins.get('modules', {}).items(): + if not re.fullmatch('[a-z0-9_]+', name) or not re.fullmatch(r'v[0-9]+\.[0-9]+\.[0-9]+(?:-standard)?', tag): + raise ValueError(f'Invalid module pin: {name}={tag}') + outputs['module_' + name] = tag + outputs['selected_v'] = 'master' if profile == 'master' else (pins['v3_v'] if profile == 'v3' else pins.get('v', pins.get('v_release', ''))) + for key, value in outputs.items(): + if not re.fullmatch('[a-z0-9_]+', key) or '\n' in value or '\r' in value: + raise ValueError(f'Unsafe compatibility output: {key}') + return outputs + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument('--profile', choices=['stable', 'v3', 'master'], default='stable') + parser.add_argument('--github-output', type=Path) + parser.add_argument('--get') + parser.add_argument('--modules', type=Path) + args = parser.parse_args() + outputs = resolve(args.root, args.profile) + if args.modules: + for key, tag in outputs.items(): + if not key.startswith('module_'): + continue + checkout = args.modules / 'antono2' / key[len('module_'):] + def revision(ref): + return subprocess.check_output(['git', '-C', str(checkout), 'rev-parse', '--verify', ref], text=True).strip() + if revision('HEAD') != revision(f'refs/tags/{tag}^{{commit}}'): + raise ValueError(f'Installed module does not match {tag}: {checkout}') + if args.github_output: + with args.github_output.open('a', encoding='utf-8') as output: + for key, value in outputs.items(): + output.write(f'{key}={value}\n') + print(outputs[args.get] if args.get else json.dumps(outputs, indent=2)) + + +if __name__ == '__main__': + main() diff --git a/scripts/doctor.py b/scripts/doctor.py new file mode 100644 index 0000000..cd4c666 --- /dev/null +++ b/scripts/doctor.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Read-only build/runtime diagnostics; use --json for a support report.""" +import argparse +import json +import os +from pathlib import Path +import re +import shutil +import subprocess + + +def probe(command, timeout=15): + try: + result = subprocess.run(command, capture_output=True, text=True, errors='replace', timeout=timeout) + return result.returncode == 0, (result.stdout + result.stderr).strip() + except (OSError, subprocess.TimeoutExpired) as error: + return False, str(error) + + +def diagnose(root, compiler=None, modules=None, sdk=None, runtime=False, profile='stable'): + checks = [] + def check(name, passed, detail, action, optional=False): + checks.append({'name': name, 'status': 'pass' if passed else ('warn' if optional else 'fail'), + 'detail': detail, 'action': '' if passed else action}) + try: + pins = json.loads((root / 'compatibility.json').read_text()) + if pins.get('schema_version') != 1: + raise ValueError('Unsupported schema_version') + except (OSError, ValueError) as error: + check('Compatibility manifest', False, str(error), 'Restore compatibility.json from this project revision.') + return {'checks': checks} + declared = dict(re.findall(r"'antono2\.([a-z0-9_]+)@([^']+)'", (root / 'v.mod').read_text())) + disagreement = [name for name, tag in declared.items() if pins.get('modules', {}).get(name) != tag] + check('Declared dependencies', not disagreement, ', '.join(disagreement) or 'Manifest agrees with v.mod', + 'Update compatibility.json and v.mod together; use the tested dependency versions.') + for tool in pins.get('build_tools', ['git', 'cmake']): + location = shutil.which(tool) + check(tool, bool(location), location or 'Not found on PATH', f'Install {tool}, reopen your shell and rerun diagnostics.') + compiler = str(compiler) if compiler else (os.environ.get('VEXE') or shutil.which('v')) + if compiler: + passed, detail = probe([compiler, '-version']) + selected = pins.get('v_release', '') if profile == 'release' else ( + pins.get('v3_v', pins.get('v', '')) if profile == 'v3' else pins.get('v', '')) + expected = selected[:7] if selected else (pins.get('v_release', '') if profile == 'stable' else '') + matches = passed and bool(expected) and expected in detail + check('V compiler', matches, detail, 'Select the tested V compiler from compatibility.json or rerun the setup helper.') + else: + check('V compiler', False, 'No V compiler found', 'Run the project setup helper, or pass --v with the tested compiler path.') + modules = modules or os.environ.get('VMODULES') + if modules: + for name, tag in pins.get('modules', {}).items(): + checkout = Path(modules) / 'antono2' / name + head_ok, head = probe(['git', '-C', str(checkout), 'rev-parse', '--verify', 'HEAD']) + tag_ok, wanted = probe(['git', '-C', str(checkout), 'rev-parse', '--verify', f'refs/tags/{tag}^{{commit}}']) + check('Module ' + name, head_ok and tag_ok and head == wanted, f'{checkout}: expected {tag}', + 'Preserve local changes and use a fresh module/cache directory with the declared releases.') + else: + check('Installed modules', False, 'VMODULES is not set', 'Pass --modules with the module directory used by setup.', optional=True) + sdk = sdk or os.environ.get('VULKAN_SDK') + needs_vulkan = 'ci_sdk' in pins or 'vulkan' in pins.get('modules', {}) + if needs_vulkan and os.name == 'nt': + available = bool(sdk) and (Path(sdk) / 'Lib/vulkan-1.lib').is_file() + check('Vulkan SDK', available, str(sdk or 'Not configured'), 'Install the Vulkan SDK or pass --sdk with its directory.') + if runtime and needs_vulkan: + executable = shutil.which('vulkaninfo') + if not executable and sdk: + candidate = Path(sdk) / ('Bin/vulkaninfo.exe' if os.name == 'nt' else 'bin/vulkaninfo') + if candidate.is_file(): + executable = str(candidate) + passed, detail = probe([executable, '--summary'], timeout=30) if executable else (False, 'vulkaninfo is unavailable') + check('Vulkan devices', passed, detail, 'Install Vulkan runtime tools and a compatible graphics driver; rerun --runtime.', optional=True) + return {'project': root.name, 'platform': os.name, 'profile': profile, 'compatibility': pins, 'checks': checks, + 'hardware_note': 'Device enumeration does not validate hardware video decoding or OpenCL interoperability.'} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--v', type=Path, help='Tested V compiler executable') + parser.add_argument('--modules', type=Path) + parser.add_argument('--sdk', type=Path) + parser.add_argument('--runtime', action='store_true', help='Also enumerate Vulkan devices, when tools are available') + parser.add_argument('--profile', choices=['stable', 'v3', 'release'], default='stable', help='Compiler profile to diagnose') + parser.add_argument('--json', action='store_true', help='Emit JSON without modifying the environment or caches') + args = parser.parse_args() + report = diagnose(Path(__file__).resolve().parents[1], args.v, args.modules, args.sdk, args.runtime, args.profile) + if args.json: + print(json.dumps(report, indent=2)) + else: + for item in report['checks']: + print(f"{item['status'].upper()}: {item['name']}: {item['detail']}") + if item['action']: + print(' Next: ' + item['action']) + raise SystemExit(1 if any(item['status'] == 'fail' for item in report['checks']) else 0) + + +if __name__ == '__main__': + main() diff --git a/scripts/test_compatibility.py b/scripts/test_compatibility.py new file mode 100644 index 0000000..2829ea6 --- /dev/null +++ b/scripts/test_compatibility.py @@ -0,0 +1,39 @@ +"""Upgrades must keep declarations and immutable compiler pins in agreement.""" +import json +from pathlib import Path +import shutil +import tempfile +import unittest +from compatibility import resolve + + +class CompatibilityChecks(unittest.TestCase): + def test_project_profile(self): + root = Path(__file__).resolve().parents[1] + pins = json.loads((root / 'compatibility.json').read_text()) + values = resolve(root) + self.assertEqual(values['selected_v'], pins.get('v', pins.get('v_release', ''))) + if 'v3_v' in pins: + self.assertEqual(resolve(root, 'v3')['selected_v'], pins['v3_v']) + self.assertEqual(resolve(root, 'master')['selected_v'], 'master') + + def test_mutable_compiler_and_disagreement_rejected(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source = Path(__file__).resolve().parents[1] + shutil.copyfile(source / 'v.mod', root / 'v.mod') + pins = json.loads((source / 'compatibility.json').read_text()) + pins['v'] = 'master' + (root / 'compatibility.json').write_text(json.dumps(pins)) + with self.assertRaisesRegex(ValueError, 'immutable'): + resolve(root) + del pins['v'] + (root / 'v.mod').write_text("Module { dependencies: ['antono2.memory@v99.0.0'] }") + pins['modules']['memory'] = 'v1.4.0' + (root / 'compatibility.json').write_text(json.dumps(pins)) + with self.assertRaisesRegex(ValueError, 'disagree'): + resolve(root) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/test_doctor.py b/scripts/test_doctor.py new file mode 100644 index 0000000..ba63017 --- /dev/null +++ b/scripts/test_doctor.py @@ -0,0 +1,56 @@ +"""Diagnostics must identify mismatches without repairing or changing caches.""" +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch +from doctor import diagnose + + +class DoctorChecks(unittest.TestCase): + def fixture(self, root): + (root / 'compatibility.json').write_text(json.dumps({'schema_version': 1, + 'v': 'a' * 40, 'modules': {'memory': 'v1.4.0'}, 'build_tools': ['git']})) + (root / 'v.mod').write_text("Module { dependencies: ['antono2.memory@v1.4.0'] }") + + def test_missing_tools_are_actionable_and_read_only(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + self.fixture(root) + before = {p.name: p.read_bytes() for p in root.iterdir()} + with patch('doctor.shutil.which', return_value=None), patch.dict('os.environ', {}, clear=True): + report = diagnose(root) + failed = [item for item in report['checks'] if item['status'] == 'fail'] + self.assertEqual({item['name'] for item in failed}, {'git', 'V compiler'}) + self.assertTrue(all(item['action'] for item in failed)) + self.assertEqual(before, {p.name: p.read_bytes() for p in root.iterdir()}) + + def test_wrong_compiler_and_dependency_are_reported(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + self.fixture(root) + (root / 'v.mod').write_text("Module { dependencies: ['antono2.memory@v99.0.0'] }") + with patch('doctor.shutil.which', return_value='git'), patch('doctor.probe', return_value=(True, 'V wrong-revision')): + report = diagnose(root, compiler='fixture-v') + failed = {item['name'] for item in report['checks'] if item['status'] == 'fail'} + self.assertIn('V compiler', failed) + self.assertIn('Declared dependencies', failed) + + def test_v3_compiler_is_checked_against_its_own_profile(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + self.fixture(root) + pins = json.loads((root / 'compatibility.json').read_text()) + pins['v3_v'] = 'b' * 40 + (root / 'compatibility.json').write_text(json.dumps(pins)) + with patch('doctor.shutil.which', return_value='git'), patch('doctor.probe', return_value=(True, 'V bbbbbbb')): + release = diagnose(root, compiler='fixture-v') + v3 = diagnose(root, compiler='fixture-v', profile='v3') + unsupported = diagnose(root, compiler='fixture-v', profile='release') + self.assertEqual(next(item for item in release['checks'] if item['name'] == 'V compiler')['status'], 'fail') + self.assertEqual(next(item for item in v3['checks'] if item['name'] == 'V compiler')['status'], 'pass') + self.assertEqual(next(item for item in unsupported['checks'] if item['name'] == 'V compiler')['status'], 'fail') + + +if __name__ == '__main__': + unittest.main()