diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..577f65b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,6 @@ +# Default owners. The GetTechAPI org has no teams yet. +* @Seungpyo1007 + +/data/ @Seungpyo1007 +/app/ @Seungpyo1007 +/.github/ @Seungpyo1007 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..2368be5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,22 @@ +name: "Bug report" +description: Report a bug in the validator, CI, or site +labels: ["bug"] +body: + - type: textarea + id: what-happened + attributes: + label: What happened? + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0086358 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/data_addition.yml b/.github/ISSUE_TEMPLATE/data_addition.yml new file mode 100644 index 0000000..1eaa4cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/data_addition.yml @@ -0,0 +1,41 @@ +name: "ES data addition" +description: Request a new engineering-sample CPU record +labels: ["data", "enhancement"] +body: + - type: dropdown + id: sample_class + attributes: + label: Sample class + options: + - es + default: 0 + validations: + required: true + - type: input + id: identifier + attributes: + label: Identifier (Intel Q-spec or AMD OPN) + placeholder: "QXLB or 100-000000665-21_N" + validations: + required: true + - type: input + id: manufacturer + attributes: + label: Manufacturer + placeholder: "intel or amd" + validations: + required: true + - type: textarea + id: sources + attributes: + label: Public source URLs (at least one) + description: Vendor leaks on public pages, CPU-World, news, benchmark listings. No NDA datasheets. + validations: + required: true + - type: checkboxes + id: not_qs + attributes: + label: Scope + options: + - label: This is an engineering sample (ES), not QS and not a retail SKU. + required: true diff --git a/.github/ISSUE_TEMPLATE/data_correction.yml b/.github/ISSUE_TEMPLATE/data_correction.yml new file mode 100644 index 0000000..b5509be --- /dev/null +++ b/.github/ISSUE_TEMPLATE/data_correction.yml @@ -0,0 +1,24 @@ +name: "ES data correction" +description: Correct an existing engineering-sample record +labels: ["data"] +body: + - type: input + id: slug + attributes: + label: Record slug + placeholder: "intel-qxlb" + validations: + required: true + - type: input + id: field + attributes: + label: Field to correct + placeholder: "boost_clock_ghz" + validations: + required: true + - type: textarea + id: correction + attributes: + label: Current value to correct value (with public source) + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..073dbf6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,23 @@ +name: "Feature request" +description: Propose a schema, validator, or site change +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem / motivation + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + validations: + required: true + - type: checkboxes + id: scope + attributes: + label: Scope check + options: + - label: This stays ES-only (QS and retail SKUs belong in TechAPI, not here). + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..5612dc9 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ + + +## What & why + + + +## Source + + + +## Checklist + +- [ ] `python -m app.validate` passes locally +- [ ] Record `sample_class` is `es` (QS / retail / production are rejected) +- [ ] Files live at `data/cpu////.json` +- [ ] Slugs are kebab-case and unique +- [ ] `source_urls` cites at least one public reference +- [ ] `qspec` (Intel) and/or `opn` (AMD) is present diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml new file mode 100644 index 0000000..fd75840 --- /dev/null +++ b/.github/workflows/deploy-pages.yml @@ -0,0 +1,39 @@ +name: deploy-pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Build static index + run: python site/build.py + - uses: actions/upload-pages-artifact@v3 + with: + path: site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/validate-data.yml b/.github/workflows/validate-data.yml new file mode 100644 index 0000000..c753484 --- /dev/null +++ b/.github/workflows/validate-data.yml @@ -0,0 +1,22 @@ +name: validate-data + +on: + pull_request: + branches: [develop, main] + push: + branches: [develop, main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Validate ES catalog + run: python -m app.validate + - name: Tests (ES pass, QS/retail fail) + run: | + python -m pip install pytest + python -m pytest -q diff --git a/.gitignore b/.gitignore index bb0c932..2344a11 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ coverage.xml # Generated site catalog (rebuilt in CI) site/catalog.json + +# Local GitHub setup helpers (not part of the catalog) +.github-setup/ diff --git a/README.md b/README.md index a9bf5e7..46241dc 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,54 @@ **Engineering-sample (ES) CPU catalog** for [GetTechAPI](https://github.com/GetTechAPI). -Retail and qualification-sample (QS) CPUs do **not** belong here. Official production SKUs live in [TechAPI](https://github.com/GetTechAPI/TechAPI). This repository is a prototype holding area for publicly documented ES parts only. +[![validate-data](https://github.com/GetTechAPI/cpu-engineering-samples/actions/workflows/validate-data.yml/badge.svg)](https://github.com/GetTechAPI/cpu-engineering-samples/actions/workflows/validate-data.yml) + Data: **CC-BY-SA 4.0** · Code: **MIT** -See the `develop` branch for the working tree. +Retail / production CPUs live in [**TechAPI**](https://github.com/GetTechAPI/TechAPI). Qualification samples (QS) are also out of scope. This repository is a **prototype** holding area for publicly documented ES parts so they never mix into the official catalog. + +## Scope + +| Belongs here | Belongs in TechAPI | +|---|---| +| Engineering samples (`sample_class: es`) | Retail / production SKUs | +| Intel Q-spec ES, AMD OPN ES | QS / qualification samples | +| Public third-party listings | Official product pages as source of truth | + +ES processors are pre-production parts. They are not for sale, not warranted, and this dataset is **not** an Intel or AMD product. Records cite already-public sources only. No NDA material. + +## Layout + +```text +data/cpu////.json +app/validate.py # stdlib validator; rejects QS/retail +site/ # static index (GitHub Pages) +``` + +Slugs identify the **sample** (`intel-qxlb`, `amd-100-000000665-21-n`), not the retail name. `retail_equivalent` is an optional TechAPI CPU slug string, not a foreign key. + +## Self-check + +```bash +python -m app.validate +python -m pytest -q +``` + +The validator uses the Python standard library. QS, retail, and production `sample_class` values fail the build. + +## Branching (git-flow) + +| Branch | Role | +|---|---| +| `main` | Released state. Pages deploys from here. | +| `develop` | Integration. Default branch. | +| `feat/*`, `data/*`, `fix/*`, `ci/*`, `docs/*`, `chore/*` | Short-lived branches cut from `develop`. | + +A release is a PR from `develop` to `main`. Open work PRs **against `develop`**. + +## Contributing + +Open a PR against `develop` with a new/updated JSON file. `sample_class` must be `es`. Include `qspec` (Intel) and/or `opn` (AMD) and at least one public `source_url`. ## License -- Code: [MIT](LICENSE) -- Data: [CC-BY-SA 4.0](DATA_LICENSE.md) +Data is [CC-BY-SA 4.0](DATA_LICENSE.md); attribute "Data from GetTechAPI / cpu-engineering-samples". Validator and site code are [MIT](LICENSE). diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..1dcda11 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +"""cpu-engineering-samples application package.""" diff --git a/app/validate.py b/app/validate.py new file mode 100644 index 0000000..39bdf05 --- /dev/null +++ b/app/validate.py @@ -0,0 +1,206 @@ +"""Validate engineering-sample CPU JSON. + +Hard rule: this catalog accepts ``sample_class == "es"`` only. +QS, retail, and production records must fail. Stdlib only. +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parent.parent +DATA_DIR = ROOT / "data" / "cpu" + +SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") +DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") +YEAR_RE = re.compile(r"^\d{4}$") + +ALLOWED_SAMPLE_CLASS = {"es"} +FORBIDDEN_SAMPLE_CLASS = { + "qs", + "qualification", + "retail", + "production", + "qs-retail", + "qs_retail", + "engineering-and-qs", +} +MANUFACTURERS = {"intel", "amd"} +SEGMENTS = {"desktop", "laptop", "hedt", "server"} +SAMPLE_REVISIONS = {"es1", "es2"} + +REQUIRED = { + "slug", + "name", + "manufacturer", + "sample_class", + "architecture", + "cores", + "threads", + "source_urls", +} + + +def _load() -> list[tuple[str, dict[str, Any]]]: + if not DATA_DIR.exists(): + return [] + records: list[tuple[str, dict[str, Any]]] = [] + for path in sorted(DATA_DIR.rglob("*.json")): + rel = str(path.relative_to(ROOT)).replace("\\", "/") + records.append((rel, json.loads(path.read_text(encoding="utf-8-sig")))) + return records + + +def _check_required(name: str, record: dict[str, Any], errors: list[str]) -> None: + missing = REQUIRED - record.keys() + if missing: + errors.append(f"{name}: missing required fields {sorted(missing)}") + + +def _check_slug(name: str, slug: object, errors: list[str]) -> None: + if not isinstance(slug, str) or not SLUG_RE.match(slug): + errors.append(f"{name}: invalid slug '{slug}' (must be kebab-case)") + + +def _check_date(name: str, field: str, value: object, errors: list[str]) -> None: + if value is None: + return + if not isinstance(value, str) or not DATE_RE.match(value): + errors.append(f"{name}: {field} '{value}' is not YYYY-MM-DD") + + +def _check_range( + name: str, field: str, value: object, lo: float, hi: float, errors: list[str] +) -> None: + if value is None: + return + if not isinstance(value, (int, float)) or isinstance(value, bool): + errors.append(f"{name}: {field} must be a number") + return + if value < lo or value > hi: + errors.append(f"{name}: {field} {value} out of range {lo}..{hi}") + + +def _check_source_urls(name: str, record: dict[str, Any], errors: list[str]) -> None: + urls = record.get("source_urls") + if not isinstance(urls, list) or not urls: + errors.append(f"{name}: source_urls must be a non-empty list") + return + for url in urls: + if not isinstance(url, str) or not url.startswith("http"): + errors.append(f"{name}: invalid source url '{url}'") + + +def _check_sample_class(name: str, record: dict[str, Any], errors: list[str]) -> None: + sample_class = record.get("sample_class") + if not isinstance(sample_class, str): + errors.append(f"{name}: sample_class is required and must be 'es'") + return + lowered = sample_class.strip().lower() + if lowered in FORBIDDEN_SAMPLE_CLASS: + errors.append( + f"{name}: sample_class '{sample_class}' is forbidden " + "(QS / retail / production belong in TechAPI, not here)" + ) + return + if lowered not in ALLOWED_SAMPLE_CLASS: + errors.append(f"{name}: sample_class '{sample_class}' must be 'es'") + + +def _check_identifiers(name: str, record: dict[str, Any], errors: list[str]) -> None: + qspec = record.get("qspec") + opn = record.get("opn") + has_qspec = isinstance(qspec, str) and qspec.strip() + has_opn = isinstance(opn, str) and opn.strip() + if not has_qspec and not has_opn: + errors.append(f"{name}: need qspec (Intel) and/or opn (AMD)") + + +def _check_path(name: str, record: dict[str, Any], errors: list[str]) -> None: + # data/cpu////.json + parts = name.split("/") + if len(parts) != 6 or parts[0] != "data" or parts[1] != "cpu": + errors.append( + f"{name}: path must be data/cpu////.json" + ) + return + _, _, manufacturer, year, segment, filename = parts + slug = record.get("slug") + if filename != f"{slug}.json": + errors.append(f"{name}: filename does not match slug '{slug}'") + if manufacturer != record.get("manufacturer"): + errors.append(f"{name}: folder manufacturer does not match record") + if not YEAR_RE.match(year): + errors.append(f"{name}: year folder '{year}' is not YYYY") + if segment != record.get("segment"): + errors.append(f"{name}: folder segment does not match record") + + +def validate(records: list[tuple[str, dict[str, Any]]] | None = None) -> list[str]: + records = _load() if records is None else records + errors: list[str] = [] + slugs: dict[str, str] = {} + + if not records: + errors.append("no ES CPU records found under data/cpu/") + return errors + + for name, rec in records: + if not isinstance(rec, dict): + errors.append(f"{name}: JSON root must be an object") + continue + _check_required(name, rec, errors) + _check_slug(name, rec.get("slug"), errors) + _check_sample_class(name, rec, errors) + _check_identifiers(name, rec, errors) + _check_source_urls(name, rec, errors) + _check_path(name, rec, errors) + _check_date(name, "first_seen_date", rec.get("first_seen_date"), errors) + _check_date(name, "release_date", rec.get("release_date"), errors) + _check_range(name, "cores", rec.get("cores"), 1, 512, errors) + _check_range(name, "threads", rec.get("threads"), 1, 1024, errors) + _check_range(name, "base_clock_ghz", rec.get("base_clock_ghz"), 0.1, 8, errors) + _check_range(name, "boost_clock_ghz", rec.get("boost_clock_ghz"), 0.1, 10, errors) + _check_range(name, "tdp_w", rec.get("tdp_w"), 1, 3000, errors) + + manufacturer = rec.get("manufacturer") + if manufacturer not in MANUFACTURERS: + errors.append( + f"{name}: manufacturer '{manufacturer}' not in {sorted(MANUFACTURERS)}" + ) + segment = rec.get("segment") + if segment not in SEGMENTS: + errors.append(f"{name}: segment '{segment}' not in {sorted(SEGMENTS)}") + revision = rec.get("sample_revision") + if revision is not None and revision not in SAMPLE_REVISIONS: + errors.append( + f"{name}: sample_revision '{revision}' not in {sorted(SAMPLE_REVISIONS)}" + ) + + slug = rec.get("slug") + if isinstance(slug, str): + if slug in slugs: + errors.append(f"{name}: duplicate slug '{slug}' (also {slugs[slug]})") + else: + slugs[slug] = name + + return errors + + +def main() -> int: + errors = validate() + if errors: + print(f"{len(errors)} validation error(s):") + for err in errors: + print(f" - {err}") + return 1 + print("OK: all ES CPU records passed (QS/retail excluded by schema).") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/data/cpu/amd/2022/desktop/amd-100-000000665-21-n.json b/data/cpu/amd/2022/desktop/amd-100-000000665-21-n.json new file mode 100644 index 0000000..b911a93 --- /dev/null +++ b/data/cpu/amd/2022/desktop/amd-100-000000665-21-n.json @@ -0,0 +1,38 @@ +{ + "slug": "amd-100-000000665-21-n", + "name": "AMD Eng Sample 100-000000665-21_N", + "manufacturer": "amd", + "sample_class": "es", + "qspec": null, + "opn": "100-000000665-21_N", + "stepping": "0", + "cpuid": "A60F00", + "sample_revision": "es1", + "retail_equivalent": "ryzen-9-7950x", + "first_seen_date": "2022-01-07", + "release_date": null, + "segment": "desktop", + "architecture": "Zen 4", + "socket": "AM5", + "process_node": "TSMC N5", + "cores": 16, + "threads": 32, + "p_cores": null, + "e_cores": null, + "base_clock_ghz": null, + "boost_clock_ghz": null, + "l3_cache_mb": null, + "tdp_w": null, + "max_tdp_w": null, + "integrated_graphics": null, + "memory_support": "DDR5", + "msrp_usd": null, + "verified": false, + "markings": ["AMD Eng Sample"], + "notes": "Raphael (Zen 4 desktop) 16-core engineering sample spotted in BOINC / Benchleaks. CPUID Family 25 Model 96 Stepping 0. 1024 KB L2 per core. Public listing only; clocks were not published with the leak.", + "source_urls": [ + "https://www.kitguru.net/components/cpu/joao-silva/amd-ryzen-7000-engineering-samples-spotted-on-benchmark-database/", + "https://twitter.com/BenchLeaks/status/1479568991731474432", + "https://en.wikipedia.org/wiki/Zen_4" + ] +} diff --git a/data/cpu/amd/2022/desktop/amd-100-000000666-21-n.json b/data/cpu/amd/2022/desktop/amd-100-000000666-21-n.json new file mode 100644 index 0000000..d901eb5 --- /dev/null +++ b/data/cpu/amd/2022/desktop/amd-100-000000666-21-n.json @@ -0,0 +1,38 @@ +{ + "slug": "amd-100-000000666-21-n", + "name": "AMD Eng Sample 100-000000666-21_N", + "manufacturer": "amd", + "sample_class": "es", + "qspec": null, + "opn": "100-000000666-21_N", + "stepping": "0", + "cpuid": "A60F00", + "sample_revision": "es1", + "retail_equivalent": "ryzen-7-7700x", + "first_seen_date": "2022-01-07", + "release_date": null, + "segment": "desktop", + "architecture": "Zen 4", + "socket": "AM5", + "process_node": "TSMC N5", + "cores": 8, + "threads": 16, + "p_cores": null, + "e_cores": null, + "base_clock_ghz": null, + "boost_clock_ghz": null, + "l3_cache_mb": null, + "tdp_w": null, + "max_tdp_w": null, + "integrated_graphics": null, + "memory_support": "DDR5", + "msrp_usd": null, + "verified": false, + "markings": ["AMD Eng Sample"], + "notes": "Raphael (Zen 4 desktop) 8-core engineering sample spotted alongside 100-000000665-21_N. Same CPUID A60F00. Public listing only; not a retail SKU.", + "source_urls": [ + "https://www.kitguru.net/components/cpu/joao-silva/amd-ryzen-7000-engineering-samples-spotted-on-benchmark-database/", + "https://twitter.com/BenchLeaks/status/1479568991731474432", + "https://en.wikipedia.org/wiki/Zen_4" + ] +} diff --git a/data/cpu/intel/2021/desktop/intel-qxlb.json b/data/cpu/intel/2021/desktop/intel-qxlb.json new file mode 100644 index 0000000..5591e42 --- /dev/null +++ b/data/cpu/intel/2021/desktop/intel-qxlb.json @@ -0,0 +1,38 @@ +{ + "slug": "intel-qxlb", + "name": "Intel QXLB Engineering Sample", + "manufacturer": "intel", + "sample_class": "es", + "qspec": "QXLB", + "opn": null, + "stepping": "B0", + "cpuid": null, + "sample_revision": "es1", + "retail_equivalent": "core-i9-12900k", + "first_seen_date": "2021-08-01", + "release_date": null, + "segment": "desktop", + "architecture": "Alder Lake", + "socket": "LGA1700", + "process_node": "Intel 7", + "cores": 16, + "threads": 24, + "p_cores": 8, + "e_cores": 8, + "base_clock_ghz": 1.2, + "boost_clock_ghz": 5.1, + "l3_cache_mb": 30.0, + "tdp_w": null, + "max_tdp_w": null, + "integrated_graphics": "Intel UHD Graphics", + "memory_support": "DDR5 / DDR4", + "msrp_usd": null, + "verified": false, + "markings": ["Intel Confidential"], + "notes": "Publicly reported Alder Lake 8P+8E ES commonly mapped to Core i9-12900K. Early samples ran a 1.2 GHz base clock; top PCIe 5.0 slot often disabled. Not a retail SKU.", + "source_urls": [ + "https://oldrigrevive.com/pt/lga-1700/lista-completa-es/", + "https://en.wikipedia.org/wiki/Alder_Lake", + "https://www.intel.com/content/www/us/en/support/articles/000056190/processors.html" + ] +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6c52a06 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "cpu-engineering-samples" +version = "0.1.0" +description = "Engineering-sample CPU catalog. QS and retail SKUs stay in TechAPI." +readme = "README.md" +requires-python = ">=3.11" +license = { text = "MIT" } + +[project.optional-dependencies] +dev = ["pytest>=8.0"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] diff --git a/site/.nojekyll b/site/.nojekyll new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/site/.nojekyll @@ -0,0 +1 @@ + diff --git a/site/build.py b/site/build.py new file mode 100644 index 0000000..407097d --- /dev/null +++ b/site/build.py @@ -0,0 +1,41 @@ +"""Build a static catalog.json from ES CPU records.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DATA = ROOT / "data" / "cpu" +OUT = Path(__file__).resolve().parent / "catalog.json" + + +def main() -> int: + records: list[dict] = [] + for path in sorted(DATA.rglob("*.json")): + rec = json.loads(path.read_text(encoding="utf-8-sig")) + records.append( + { + "slug": rec["slug"], + "name": rec["name"], + "manufacturer": rec["manufacturer"], + "sample_class": rec["sample_class"], + "qspec": rec.get("qspec"), + "opn": rec.get("opn"), + "stepping": rec.get("stepping"), + "architecture": rec.get("architecture"), + "cores": rec.get("cores"), + "threads": rec.get("threads"), + "retail_equivalent": rec.get("retail_equivalent"), + "first_seen_date": rec.get("first_seen_date"), + "source_urls": rec.get("source_urls", []), + } + ) + OUT.write_text(json.dumps(records, indent=2) + "\n", encoding="utf-8") + print(f"wrote {OUT} ({len(records)} ES records)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..3b0f5a8 --- /dev/null +++ b/site/index.html @@ -0,0 +1,134 @@ + + + + + + cpu-engineering-samples — ES CPU catalog + + + +
+

Engineering-sample CPUs

+

+ Prototype catalog for + GetTechAPI. Retail and QS + SKUs stay in + TechAPI. +

+ + + + + + + + + + + + + + + + +
SampleIDCoresRetail equivalentSources
Loading catalog…
+
+ + + diff --git a/tests/test_validate.py b/tests/test_validate.py new file mode 100644 index 0000000..3a3e6c8 --- /dev/null +++ b/tests/test_validate.py @@ -0,0 +1,72 @@ +"""Validator accepts ES records and rejects QS / retail.""" + +from __future__ import annotations + +import copy +import json +from pathlib import Path + +from app import validate + +ROOT = Path(__file__).resolve().parent.parent +SEED = ROOT / "data" / "cpu" / "intel" / "2021" / "desktop" / "intel-qxlb.json" + + +def _seed() -> dict: + return json.loads(SEED.read_text(encoding="utf-8")) + + +def test_seed_catalog_passes() -> None: + errors = validate.validate() + assert errors == [] + + +def test_qs_sample_class_rejected() -> None: + rec = _seed() + rec["sample_class"] = "qs" + rec["qspec"] = "QDF4" + rec["slug"] = "intel-qdf4" + errors = validate.validate([("data/cpu/intel/2023/desktop/intel-qdf4.json", rec)]) + assert any("forbidden" in err or "qs" in err.lower() for err in errors) + + +def test_retail_sample_class_rejected() -> None: + rec = _seed() + rec["sample_class"] = "retail" + rec["slug"] = "core-i9-12900k" + rec["name"] = "Intel Core i9-12900K" + errors = validate.validate( + [("data/cpu/intel/2021/desktop/core-i9-12900k.json", rec)] + ) + assert any("forbidden" in err or "retail" in err.lower() for err in errors) + + +def test_production_sample_class_rejected() -> None: + rec = _seed() + rec["sample_class"] = "production" + errors = validate.validate([("data/cpu/intel/2021/desktop/intel-qxlb.json", rec)]) + assert any("forbidden" in err for err in errors) + + +def test_missing_sample_class_rejected() -> None: + rec = _seed() + del rec["sample_class"] + errors = validate.validate([("data/cpu/intel/2021/desktop/intel-qxlb.json", rec)]) + assert any("sample_class" in err for err in errors) + + +def test_missing_identifier_rejected() -> None: + rec = _seed() + rec["qspec"] = None + rec["opn"] = None + errors = validate.validate([("data/cpu/intel/2021/desktop/intel-qxlb.json", rec)]) + assert any("qspec" in err and "opn" in err for err in errors) + + +def test_unknown_manufacturer_rejected() -> None: + rec = copy.deepcopy(_seed()) + rec["manufacturer"] = "contoso" + errors = validate.validate( + [("data/cpu/contoso/2021/desktop/intel-qxlb.json", rec)] + ) + assert any("manufacturer" in err for err in errors)