Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/bcbench/operations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@
from bcbench.operations.hooks_operations import setup_hooks
from bcbench.operations.instruction_operations import copy_problem_statement_folder, setup_custom_agent, setup_instructions_from_config
from bcbench.operations.project_operations import categorize_projects
from bcbench.operations.setup_operations import set_runtime_version, setup_repo_prebuild
from bcbench.operations.setup_operations import bootstrap_app_json, set_runtime_version, setup_repo_prebuild
from bcbench.operations.skills_operations import setup_agent_skills
from bcbench.operations.test_operations import extract_tests_from_patch

__all__ = [
"apply_patch",
"bootstrap_app_json",
"build_and_publish_projects",
"build_ps_app_build_and_publish",
"build_ps_dataset_tests_script",
Expand Down
75 changes: 71 additions & 4 deletions src/bcbench/operations/setup_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

import json
from pathlib import Path
from uuid import uuid4

from bcbench.dataset.dataset_entry import RepoGroundedEntry
from bcbench.logger import get_logger
from bcbench.operations.git_operations import checkout_commit, clean_repo

logger = get_logger(__name__)

__all__ = ["set_runtime_version", "setup_repo_prebuild"]
__all__ = ["bootstrap_app_json", "set_runtime_version", "setup_repo_prebuild"]

# Offset from BC platform major version to AL runtime version.
# E.g. platform 25.0 (BC 2024w2) → runtime 14.0, platform 27.0 → runtime 16.0
Expand All @@ -31,6 +32,73 @@ def setup_repo_prebuild(entry: RepoGroundedEntry, repo_path: Path) -> None:
checkout_commit(repo_path, entry.base_commit)


def bootstrap_app_json(
app_folder: Path,
name: str,
bc_version: str,
*,
id_range: tuple[int, int] = (50100, 50149),
publisher: str = "BC-Bench",
app_version: str = "1.0.0.0",
target: str = "OnPrem",
app_id: str | None = None,
) -> Path:
"""Write a minimal, compilable ``app.json`` into ``app_folder``.

Every AL app needs an ``app.json``; this bootstraps one for throwaway apps built by the
harness (e.g. wrapping a generated query or test codeunit) so categories don't hand-roll manifests.

Args:
app_folder: Folder to create (if missing) and write ``app.json`` into.
name: App name, also used as the publisher-facing app name.
bc_version: BC platform version, e.g. ``"26.0.12345.0"`` or ``"26.0"``; its major version
drives ``platform``, ``application`` and the derived ``runtime``.
id_range: Inclusive object ID range for the app.
publisher: App publisher.
app_version: App version.
target: App target, e.g. ``"OnPrem"`` or ``"Cloud"``.
app_id: App GUID; a random one is generated when omitted.

Returns:
Path to the written ``app.json``.
"""
major = _major_version(bc_version)
if major is None:
raise ValueError(f"Cannot derive major version from BC version: {bc_version!r}")

id_from, id_to = id_range
if id_from > id_to:
raise ValueError(f"Invalid id_range: {id_range!r}")

manifest: dict[str, object] = {
"id": app_id or str(uuid4()),
"name": name,
"publisher": publisher,
"version": app_version,
"platform": f"{major}.0.0.0",
"application": f"{major}.0.0.0",
"idRanges": [{"from": id_from, "to": id_to}],
"target": target,
}

runtime_major = major - _PLATFORM_TO_RUNTIME_OFFSET
if runtime_major >= 1:
manifest["runtime"] = f"{runtime_major}.0"

app_folder.mkdir(parents=True, exist_ok=True)
app_json_path = app_folder / "app.json"
app_json_path.write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")
logger.info(f"Bootstrapped {app_json_path} (name={name}, platform {major}.0.0.0, ids {id_from}-{id_to})")
return app_json_path


def _major_version(version: str) -> int | None:
try:
return int(version.split(".")[0])
except (ValueError, IndexError, AttributeError):
return None


def set_runtime_version(repo_path: Path, project_paths: list[str]) -> None:
"""Set the AL runtime version in each project's app.json based on platform version.

Expand All @@ -53,9 +121,8 @@ def set_runtime_version(repo_path: Path, project_paths: list[str]) -> None:
continue

platform: str = app_json.get("platform", "")
try:
platform_major = int(platform.split(".")[0])
except (ValueError, IndexError):
platform_major = _major_version(platform)
if platform_major is None:
continue

runtime_major: int = platform_major - _PLATFORM_TO_RUNTIME_OFFSET
Expand Down
53 changes: 52 additions & 1 deletion tests/test_setup_operations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import json
import uuid

from bcbench.operations.setup_operations import set_runtime_version
import pytest

from bcbench.operations.setup_operations import bootstrap_app_json, set_runtime_version


class TestSetRuntimeVersion:
Expand Down Expand Up @@ -33,3 +36,51 @@ def test_platform_27_maps_to_runtime_16(self, tmp_path):

def test_skips_missing_app_json(self, tmp_path):
set_runtime_version(tmp_path, [str(tmp_path)]) # should not raise


class TestBootstrapAppJson:
def test_writes_manifest_with_derived_versions(self, tmp_path):
path = bootstrap_app_json(tmp_path / "app", "BC-Bench Query", "26.0.12345.0")

manifest = json.loads(path.read_text())
assert path == tmp_path / "app" / "app.json"
assert manifest["name"] == "BC-Bench Query"
assert manifest["publisher"] == "BC-Bench"
assert manifest["platform"] == manifest["application"] == "26.0.0.0"
assert manifest["runtime"] == "15.0"
assert manifest["target"] == "OnPrem"
assert manifest["idRanges"] == [{"from": 50100, "to": 50149}]
assert uuid.UUID(manifest["id"])

def test_overrides_are_applied(self, tmp_path):
app_id = "1e6a4e1f-4b0e-4b1f-9d0a-1a2b3c4d5e6f"
path = bootstrap_app_json(
tmp_path,
"Custom",
"25.0",
id_range=(50000, 50001),
publisher="Contoso",
app_version="2.1.0.0",
target="Cloud",
app_id=app_id,
)

manifest = json.loads(path.read_text())
assert manifest["id"] == app_id
assert manifest["publisher"] == "Contoso"
assert manifest["version"] == "2.1.0.0"
assert manifest["target"] == "Cloud"
assert manifest["idRanges"] == [{"from": 50000, "to": 50001}]

def test_omits_runtime_when_platform_too_old(self, tmp_path):
manifest = json.loads(bootstrap_app_json(tmp_path, "Old", "10.0.0.0").read_text())

assert "runtime" not in manifest

def test_rejects_unparsable_version(self, tmp_path):
with pytest.raises(ValueError, match="major version"):
bootstrap_app_json(tmp_path, "Bad", "not-a-version")

def test_rejects_inverted_id_range(self, tmp_path):
with pytest.raises(ValueError, match="id_range"):
bootstrap_app_json(tmp_path, "Bad", "26.0", id_range=(50200, 50100))
Loading