Added testing

Preparing profiles and dependencies, but added testing to separate them from regular targets and avoid the need for a 'testing' profile which could negatively impact configuration size and readability.
This commit is contained in:
2026-06-26 03:45:38 +00:00
parent 24b46f7fc2
commit b7b99783c8
24 changed files with 612 additions and 215 deletions
+7
View File
@@ -6,3 +6,10 @@ indent_style = tab
indent_size = 2 indent_size = 2
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[*.yaml,chinookfile]
charset = utf-8
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
+44 -5
View File
@@ -20,14 +20,42 @@ def _build(args: Namespace) -> None:
try: try:
projpath = Path(args.project) projpath = Path(args.project)
cfgpath = projpath / "chinookconfig" cfgpath = projpath / "chinookconfig"
return pipeline.build( appcfg = Config.create_default_or_load(cfgpath)
Config.create_default_or_load(cfgpath), projcfg = management.load(projpath)
management.load(projpath) result = pipeline.build(appcfg, projcfg, logger)
) logger.info(f"[100%] Built {projcfg.cid}")
return result
except Exception as err: except Exception as err:
logger.exception(str(err)) logger.exception(str(err))
logger.critical("Failed to build project") logger.critical("Failed to build project")
def _test(args: Namespace) -> None:
from chinook import management
from chinook import pipeline
from chinook._config import Config
from chinook._logger import logger
try:
projpath = Path(args.project)
cfgpath = projpath / "chinookconfig"
appcfg = Config.create_default_or_load(cfgpath)
projcfg = management.load(projpath)
result = pipeline.test(appcfg, projcfg, logger)
passed = 0
count = 0
for item in result:
if item.action == "Testing":
count += 1
if item.retcode == 0:
passed += 1
logger.info(f"Tested {projcfg.cid} [{passed}/{count}]")
except Exception as err:
logger.exception(str(err))
logger.critical("Failed to test project")
def main() -> None: def main() -> None:
print("\n".join([ print("\n".join([
@@ -50,7 +78,18 @@ def main() -> None:
nargs="?", nargs="?",
default=".", default=".",
type=str, type=str,
help="The path to the project to build", help="The path to the project to build.",
metavar="PROJECT"
)
testcmd = commands.add_parser("test",help="Tests your Chinook project")
testcmd.set_defaults(func=_test)
testcmd.add_argument(
"project",
nargs="?",
default=".",
type=str,
help="The path to the project to test.",
metavar="PROJECT" metavar="PROJECT"
) )
+13 -5
View File
@@ -1,7 +1,8 @@
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses import field from dataclasses import field
from logging import getLogger
from pathlib import Path from logging import Logger
from pathlib import Path
from ..models import Abi from ..models import Abi
from ..models import Result from ..models import Result
@@ -13,6 +14,8 @@ from ._mingw import MingwDriver
from ._msvc import MsvcDriver from ._msvc import MsvcDriver
from ._options import CompilerOptions from ._options import CompilerOptions
_logger = getLogger("chinook")
@dataclass @dataclass
class Compiler: class Compiler:
@@ -25,10 +28,11 @@ class Compiler:
} }
tooling: Tooling tooling: Tooling
logger: Logger = field(default=_logger)
_driver: CompilerDriver = field(init=False) _driver: CompilerDriver = field(init=False)
def __post_init__(self) -> None: def __post_init__(self) -> None:
self._driver = Compiler._driver_for(self.tooling) self._driver = Compiler._driver_for(self.tooling, self.logger)
def compile_program( def compile_program(
self, self,
@@ -60,8 +64,12 @@ class Compiler:
return self._driver.compile_object(options, srcpath, dstPath) return self._driver.compile_object(options, srcpath, dstPath)
@classmethod @classmethod
def _driver_for(cls, tooling: Tooling) -> CompilerDriver: def _driver_for(
cls,
tooling: Tooling,
logger: Logger
) -> CompilerDriver:
from ..models import Triple from ..models import Triple
triples = tooling.when or [Triple.current()] triples = tooling.when or [Triple.current()]
driver = cls.__COMPILER_MAPPING.get(triples[0].abi, GnucDriver) driver = cls.__COMPILER_MAPPING.get(triples[0].abi, GnucDriver)
return driver(tooling) return driver(tooling, logger)
+12 -4
View File
@@ -1,18 +1,26 @@
from abc import ABC from abc import ABC
from abc import abstractmethod from abc import abstractmethod
from logging import getLogger
from logging import Logger
from pathlib import Path from pathlib import Path
from ..models import Tooling from ..models import Tooling
from ..models import Result from ..models import Result
from ._options import CompilerOptions from ._options import CompilerOptions
_logger = getLogger("chinook")
class CompilerDriver(ABC): class CompilerDriver(ABC):
def __init__(self, tooling: Tooling) -> None: def __init__(
self,
tooling: Tooling,
logger: Logger = _logger
) -> None:
if tooling is None: if tooling is None:
raise ValueError raise ValueError
self.tooling = tooling self.tooling = tooling
self.logger = logger
@abstractmethod @abstractmethod
def compile_program( def compile_program(
+86 -67
View File
@@ -2,10 +2,7 @@ from pathlib import Path
from ..models import Optimization from ..models import Optimization
from ..models import Result from ..models import Result
from ..models import StdC
from ..models import StdCpp
from ..shell import execute from ..shell import execute
from .._logger import logger
from ._driver import CompilerDriver from ._driver import CompilerDriver
from ._options import CompilerOptions from ._options import CompilerOptions
@@ -48,17 +45,30 @@ class GnucDriver(CompilerDriver):
for directory in options.libdirs: args.append(f"-L{str(directory)}") for directory in options.libdirs: args.append(f"-L{str(directory)}")
for archive in options.archives: args.append(f"-l{archive}") for archive in options.archives: args.append(f"-l{archive}")
cmdstr = ' '.join(args) cmdstr = ' '.join(args)
# logger.info(cmdstr) # self.logger.info(cmdstr)
result: list[Result] = [None] result = Result(
execute( success = False,
cmdstr, action = "Linked",
lambda cmdstr, stdout, stderr: target = str(dstpath),
self.handle_on_data(result, str(dstpath), cmdstr, stdout, stderr), command = cmdstr,
lambda cmdstr, stdout, stderr: retcode = -1
self.handle_on_error(result, str(dstpath), cmdstr, stdout, stderr)
) )
return result[0]
def handle_success(retcode: int, cmdstr: str, stdout: str) -> None:
result.success = True
result.command = cmdstr
result.retcode = retcode
result.diagnostics.append(stdout)
def handle_failure(retcode: int, cmdstr: str, stderr: str) -> None:
result.success = False
result.command = cmdstr
result.retcode = retcode
result.diagnostics.append(stderr)
execute(cmdstr, handle_success, handle_failure)
return result
def compile_archive( def compile_archive(
self, self,
@@ -70,15 +80,28 @@ class GnucDriver(CompilerDriver):
args.append(str(obj)) args.append(str(obj))
cmdstr = ' '.join(args) cmdstr = ' '.join(args)
result: list[Result] = [None] result = Result(
execute( success = False,
cmdstr, action = "Linked",
lambda cmdstr, stdout, stderr: target = str(dstpath),
self.handle_on_data(result, str(dstpath), cmdstr, stdout, stderr), command = cmdstr,
lambda cmdstr, stdout, stderr: retcode = -1
self.handle_on_error(result, str(dstpath), cmdstr, stdout, stderr)
) )
return result[0]
def handle_success(retcode: int, cmdstr: str, stdout: str) -> None:
result.success = True
result.command = cmdstr
result.retcode = retcode
result.diagnostics.append(stdout)
def handle_failure(retcode: int, cmdstr: str, stderr: str) -> None:
result.success = False
result.command = cmdstr
result.retcode = retcode
result.diagnostics.append(stderr)
execute(cmdstr, handle_success, handle_failure)
return result
def compile_library( def compile_library(
self, self,
@@ -106,17 +129,30 @@ class GnucDriver(CompilerDriver):
for directory in options.libdirs: args.append(f"-L{str(directory)}") for directory in options.libdirs: args.append(f"-L{str(directory)}")
for archive in options.archives: args.append(f"-l{archive}") for archive in options.archives: args.append(f"-l{archive}")
cmdstr = ' '.join(args) cmdstr = ' '.join(args)
# logger.info(cmdstr) # self.logger.info(cmdstr)
result: list[Result] = [None] result = Result(
execute( success = False,
cmdstr, action = "Linked",
lambda cmdstr, stdout, stderr: target = str(dstpath),
self.handle_on_data(result, str(dstpath), cmdstr, stdout, stderr), command = cmdstr,
lambda cmdstr, stdout, stderr: retcode = -1
self.handle_on_error(result, str(dstpath), cmdstr, stdout, stderr)
) )
return result[0]
def handle_success(retcode: int, cmdstr: str, stdout: str) -> None:
result.success = True
result.command = cmdstr
result.retcode = retcode
result.diagnostics.append(stdout)
def handle_failure(retcode: int, cmdstr: str, stderr: str) -> None:
result.success = False
result.command = cmdstr
result.retcode = retcode
result.diagnostics.append(stderr)
execute(cmdstr, handle_success, handle_failure)
return result
def compile_object( def compile_object(
self, self,
@@ -150,44 +186,27 @@ class GnucDriver(CompilerDriver):
for archive in options.archives: args.append(f"-l{archive}") for archive in options.archives: args.append(f"-l{archive}")
for define in options.defines: args.append(f"-D{define}") for define in options.defines: args.append(f"-D{define}")
cmdstr = ' '.join(args) cmdstr = ' '.join(args)
# logger.info(cmdstr) # self.logger.info(cmdstr)
result: list[Result] = [None] result = Result(
execute(
cmdstr,
lambda cmdstr, stdout, stderr:
self.handle_on_data(result, str(dstpath), cmdstr, stdout, stderr),
lambda cmdstr, stdout, stderr:
self.handle_on_error(result, str(dstpath), cmdstr, stdout, stderr)
)
return result[0]
def handle_on_data(
self,
result: list[Result],
target: str,
cmdstr: str,
stdout: str,
stderr: str
) -> None:
result[0] = Result(
success = True,
target = target,
command = cmdstr
)
result[0].diagnostics.append(stdout)
def handle_on_error(
self,
result: list[Result],
target: str,
cmdstr: str,
stdout: str,
stderr: str
) -> None:
result[0] = Result(
success = False, success = False,
target = target, action = "Compiled",
command = cmdstr target = str(dstpath),
command = cmdstr,
retcode = -1
) )
result[0].diagnostics.append(stderr)
def handle_success(retcode: int, cmdstr: str, stdout: str) -> None:
result.success = True
result.command = cmdstr
result.retcode = retcode
result.diagnostics.append(stdout)
def handle_failure(retcode: int, cmdstr: str, stderr: str) -> None:
result.success = False
result.command = cmdstr
result.retcode = retcode
result.diagnostics.append(stderr)
execute(cmdstr, handle_success, handle_failure)
return result
-1
View File
@@ -2,7 +2,6 @@ from typing import Optional
from ..models import Project from ..models import Project
from ..models import Target from ..models import Target
from ._clone import clone
from ._except import NoProjectFoundError from ._except import NoProjectFoundError
from ._latest import latest from ._latest import latest
from ._load import load from ._load import load
+7
View File
@@ -32,6 +32,7 @@ def load(path: Path) -> Project:
project = Project.from_dict(yamlobj) project = Project.from_dict(yamlobj)
project.origination = Origination(path) project.origination = Origination(path)
project.destination = Destination(path) project.destination = Destination(path)
_collect_profiles(project)
_collect_required(project) _collect_required(project)
_collect_targets(project) _collect_targets(project)
_flatten_targets(project) _flatten_targets(project)
@@ -39,6 +40,12 @@ def load(path: Path) -> Project:
except Exception as rethrowme: except Exception as rethrowme:
raise rethrowme raise rethrowme
def _collect_profiles(project: Project) -> None:
# TODO: Determine what profile is supposed to be active and load the targets
# so that they will be compiled.
for profile in project.profiles:
...
def _collect_required(project: Project) -> None: def _collect_required(project: Project) -> None:
for required in project.requires: for required in project.requires:
_clone_then_load_required(required) _clone_then_load_required(required)
+6 -2
View File
@@ -58,8 +58,12 @@ class Dependency(yaml.YAMLError):
return cls(**data) return cls(**data)
@classmethod @classmethod
def from_list(cls, data: list[str]) -> list[Self]: def from_list(cls, data: list[dict | Self]) -> list[Self]:
return [cls.from_dict(v) for v in data] return [
v if isinstance(v, Dependency) \
else cls.from_dict(v)
for v in data
]
@classmethod @classmethod
def from_yaml( def from_yaml(
+88
View File
@@ -0,0 +1,88 @@
import yaml
from dataclasses import dataclass
from dataclasses import field
from typing import ClassVar
from typing import Self
from ._triple import Triple
from ._target import Target
@dataclass
class Profile(yaml.YAMLObject):
yaml_dumper: ClassVar[type[yaml.SafeDumper]] = yaml.SafeDumper
yaml_loader: ClassVar[type[yaml.SafeLoader]] = yaml.SafeLoader
yaml_tag: ClassVar[str] = u"!profile"
name: str
when: list[Triple] = field(default_factory=list)
targets: list[Target] = field(default_factory=list)
@property
def native(self) -> bool:
return not self.when or any(t.native for t in self.when)
@classmethod
def to_dict(cls, data: Self) -> dict:
return {
"name": data.name,
"when": Triple.to_list(data.when),
"targets": Target.to_list(data.targets)
}
@classmethod
def to_list(cls, data: list[Self]) -> list[dict]:
return [cls.to_dict(v) for v in data]
@classmethod
def to_yaml(
cls,
dumper: yaml.SafeDumper,
data: yaml.YAMLObject
) -> yaml.Node:
if not isinstance(data, Profile):
raise ValueError
return dumper.represent_mapping(
"tag:yaml.org,2002:map",
cls.to_dict(data)
)
@classmethod
def from_dict(cls, data: dict) -> Self:
def bad_attr(name: str, obj: dict) -> None:
raise ArithmeticError(name=name,obj=obj)
return cls(**{
"name": data.get("name") or bad_attr("name", data),
"when": Triple.from_list(data.get("when", [])),
"targets": Target.from_list(data.get("targets", []))
})
@classmethod
def from_list(cls, data: list[dict | Self]) -> list[Self]:
return [
v if isinstance(v, Profile) \
else cls.from_dict(v)
for v in data
]
@classmethod
def from_yaml(
cls,
loader: yaml.SafeLoader,
node: yaml.Node
) -> Self:
return cls.from_dict(loader.construct_mapping(node, deep=True))
Profile.yaml_dumper.add_representer(
Profile.yaml_tag,
Profile.to_yaml
)
Profile.yaml_loader.add_constructor(
Profile.yaml_tag,
Profile.from_yaml
)
+17
View File
@@ -9,6 +9,7 @@ from ._dependency import Dependency
from ._destination import Destination from ._destination import Destination
from ._identity import Identity from ._identity import Identity
from ._origination import Origination from ._origination import Origination
from ._profile import Profile
from ._target import Target from ._target import Target
@@ -18,8 +19,10 @@ class Project(Identity):
yaml_loader: ClassVar[type[yaml.SafeLoader]] = yaml.SafeLoader yaml_loader: ClassVar[type[yaml.SafeLoader]] = yaml.SafeLoader
yaml_tag: ClassVar[str] = u"!project" yaml_tag: ClassVar[str] = u"!project"
profiles: list[Profile] = field(default_factory=list)
requires: list[Dependency] = field(default_factory=list) requires: list[Dependency] = field(default_factory=list)
targets: list[Target] = field(default_factory=list) targets: list[Target] = field(default_factory=list)
tests: list[Target] = field(default_factory=list)
exports: list[str] = field(default_factory=list) exports: list[str] = field(default_factory=list)
origination: Origination = field(default=Origination.cwd()) origination: Origination = field(default=Origination.cwd())
@@ -28,8 +31,10 @@ class Project(Identity):
@classmethod @classmethod
def to_dict(cls, data: Self) -> dict: def to_dict(cls, data: Self) -> dict:
return { return {
"profiles": Profile.to_list(data.profiles),
"requires": Dependency.to_list(data.requires), "requires": Dependency.to_list(data.requires),
"targets": Target.to_list(data.targets), "targets": Target.to_list(data.targets),
"tests": Target.to_list(data.targets),
"exports": data.exports "exports": data.exports
} }
@@ -53,8 +58,10 @@ class Project(Identity):
"name": data.get("name") or bad_attr("name", data), "name": data.get("name") or bad_attr("name", data),
"gpid": data.get("gpid") or bad_attr("gpid", data), "gpid": data.get("gpid") or bad_attr("gpid", data),
"semv": data.get("semv") or bad_attr("semv", data), "semv": data.get("semv") or bad_attr("semv", data),
"profiles": Profile.from_list(data.get("profiles", [])),
"requires": Dependency.from_list(data.get("requires", [])), "requires": Dependency.from_list(data.get("requires", [])),
"targets": Target.from_list(data.get("targets", [])), "targets": Target.from_list(data.get("targets", [])),
"tests": Target.from_list(data.get("tests", [])),
"exports": data.get("exports", []) "exports": data.get("exports", [])
}) })
@@ -67,3 +74,13 @@ class Project(Identity):
return cls.from_dict( return cls.from_dict(
loader.construct_mapping(node, deep=True) loader.construct_mapping(node, deep=True)
) )
Project.yaml_dumper.add_representer(
Project.yaml_tag,
Project.to_yaml
)
Project.yaml_loader.add_constructor(
Project.yaml_tag,
Project.from_yaml
)
+2
View File
@@ -5,6 +5,8 @@ from dataclasses import field
@dataclass @dataclass
class Result: class Result:
success: bool success: bool
action: str
target: str target: str
command: str command: str
retcode: int
diagnostics: list[str] = field(default_factory=list) diagnostics: list[str] = field(default_factory=list)
+3 -7
View File
@@ -54,18 +54,14 @@ class Tooling(yaml.YAMLObject):
def from_dict(cls, data: dict) -> Self: def from_dict(cls, data: dict) -> Self:
return cls(**{ return cls(**{
**data, **data,
"when": [ "when": Triple.from_list(data.get("when", []))
v if isinstance(v, Triple) \
else Triple.from_string(v)
for v in data.get("when", [])
]
}) })
@classmethod @classmethod
def from_list(cls, data: list[dict]) -> list[Self]: def from_list(cls, data: list[dict | Self]) -> list[Self]:
return [ return [
v if isinstance(v, Tooling) \ v if isinstance(v, Tooling) \
else Tooling.from_dict(v) else cls.from_dict(v)
for v in data for v in data
] ]
+24 -14
View File
@@ -24,7 +24,7 @@ class Arch(enum.StrEnum):
S390X = "s390x" S390X = "s390x"
WASM32 = "wasm32" WASM32 = "wasm32"
WASM64 = "wasm64" WASM64 = "wasm64"
UNKNOWN = "unknown" ANY = "any"
class Sys(enum.StrEnum): class Sys(enum.StrEnum):
LINUX = "linux" LINUX = "linux"
@@ -36,8 +36,7 @@ class Sys(enum.StrEnum):
ANDROID = "android" ANDROID = "android"
IOS = "ios" IOS = "ios"
WASI = "wasi" WASI = "wasi"
NONE = "none" ANY = "any"
UNKNOWN = "unknown"
class Abi(enum.StrEnum): class Abi(enum.StrEnum):
GNU = "gnu" GNU = "gnu"
@@ -45,8 +44,7 @@ class Abi(enum.StrEnum):
MINGW = "mingw" MINGW = "mingw"
ANDROID = "android" ANDROID = "android"
ELF = "elf" ELF = "elf"
NONE = "none" ANY = "any"
UNKNOWN = "unknown"
@dataclass @dataclass
@@ -91,9 +89,9 @@ class Triple(yaml.YAMLObject):
def native(self) -> bool: def native(self) -> bool:
host = Triple.current() host = Triple.current()
return ( return (
(self.arch == Arch.UNKNOWN or self.arch == host.arch) and (self.arch == Arch.ANY or self.arch == host.arch) and
(self.sys == Sys.UNKNOWN or self.sys == host.sys) and (self.sys == Sys.ANY or self.sys == host.sys) and
(self.abi == Abi.UNKNOWN or self.abi == host.abi) (self.abi == Abi.ANY or self.abi == host.abi)
) )
def __str__(self) -> str: def __str__(self) -> str:
@@ -111,6 +109,10 @@ class Triple(yaml.YAMLObject):
def to_string(cls, triple: Self) -> str: def to_string(cls, triple: Self) -> str:
return str(triple) return str(triple)
@classmethod
def to_list(cls, data: list[Self]) -> list[str]:
return [cls.to_string(v) for v in data]
@classmethod @classmethod
def to_yaml( def to_yaml(
cls, cls,
@@ -141,11 +143,19 @@ class Triple(yaml.YAMLObject):
return fallback return fallback
return cls( return cls(
arch = _coerce(Arch, parts[0].lower(), Arch.UNKNOWN), arch = _coerce(Arch, parts[0].lower(), Arch.ANY),
sys = _coerce(Sys, parts[1].lower(), Sys.UNKNOWN), sys = _coerce(Sys, parts[1].lower(), Sys.ANY),
abi = _coerce(Abi, parts[2].lower(), Abi.UNKNOWN) abi = _coerce(Abi, parts[2].lower(), Abi.ANY)
) )
@classmethod
def from_list(cls, data: list[str | Self]) -> list[Self]:
return [
v if isinstance(v, Triple) \
else cls.from_string(v)
for v in data
]
@classmethod @classmethod
def from_yaml( def from_yaml(
cls, cls,
@@ -157,18 +167,18 @@ class Triple(yaml.YAMLObject):
@classmethod @classmethod
def _select_arch(cls) -> Arch: def _select_arch(cls) -> Arch:
curr = platform.machine().lower() curr = platform.machine().lower()
return cls.__arch_map.get(curr, Arch.UNKNOWN) return cls.__arch_map.get(curr, Arch.ANY)
@classmethod @classmethod
def _select_sys(cls) -> Sys: def _select_sys(cls) -> Sys:
curr = platform.system().lower() curr = platform.system().lower()
return cls.__sys_map.get(curr, Sys.UNKNOWN) \ return cls.__sys_map.get(curr, Sys.ANY) \
if system.platform != "android" \ if system.platform != "android" \
else Sys.ANDROID else Sys.ANDROID
@classmethod @classmethod
def _select_abi(cls, arch: Arch, sys: Sys) -> Abi: def _select_abi(cls, arch: Arch, sys: Sys) -> Abi:
res = Abi.UNKNOWN res = Abi.ANY
if sys == Sys.WINDOWS: if sys == Sys.WINDOWS:
res = Abi.MSVC res = Abi.MSVC
elif sys == Sys.ANDROID: elif sys == Sys.ANDROID:
+4 -1
View File
@@ -1 +1,4 @@
from ._build import build from ._build import build
from ._commands import Command
from ._prepare import prepare
from ._test import test
+18 -98
View File
@@ -1,106 +1,26 @@
from pathlib import Path from logging import getLogger
from logging import Logger
from ..compiler import Compiler from ..models import Project
from ..compiler import CompilerOptions from ..models import Result
from ..management import latest from .._config import Config
from ..management import find_target_by_name from ._commands import Command
from ..management import find_project_by_target from ._except import NoTargetsFoundError
from ..models import Access from ._execute import execute
from ..models import Accessor from ._prepare import prepare
from ..models import Output
from ..models import Project _logger = getLogger("chinook")
from ..models import Result
from ..models import Target
from .._config import Config
from .._logger import logger
from ._commands import Command
from ._commands import CompileArchive
from ._commands import CompileDynlib
from ._commands import CompileObject
from ._commands import CompileProgram
from ._except import NoTargetsFoundError
def build(appcfg: Config, project: Project) -> list[Result]: def build(
appcfg: Config,
project: Project,
logger: Logger = _logger
) -> list[Result]:
if len(project.targets) == 0: if len(project.targets) == 0:
raise NoTargetsFoundError(project) raise NoTargetsFoundError(project)
# Put together list of commands and then execute them.
commands: list[Command] = [] commands: list[Command] = []
results: list[Result] = []
for target in project.targets: for target in project.targets:
_prepare_build_commands(appcfg, project, target, commands) commands.extend(prepare(appcfg, project, target, logger))
return execute(commands, "Building")
executed: int = 0
cmdcount: int = len(commands)
for command in commands:
result = command.execute()
results.append(result)
percentage = (executed / cmdcount)
if result.success:
logger.info(f"[{percentage:.0%}] Compiled {result.target}")
executed += 1
else:
logger.error(f"[{percentage:.0%}] Failed {result.target}")
for diagnostic in result.diagnostics:
logger.error(f"Because {diagnostic}")
return
logger.info(f"[100%] Built {project.cid}")
def _prepare_build_commands(
appcfg: Config,
project: Project,
target: Target,
commands: list[Command]
) -> None:
objects: list[Path] = []
# TODO: clean this up.
compiler = Compiler(appcfg.native_tooling)
defopts = CompilerOptions()
defopts.pic = target.type == Output.DLL
defopts.incdirs.update([v.value for v in target.incs.includes])
defopts.defines.update([v.value for v in target.defs])
defopts.flags.update([v.value for v in target.opts])
for file in target.srcs:
srcfile = project.origination.src(file.value)
dstfile = project.destination.obj(file.value)
if latest(srcfile, dstfile):
continue
commands.append(CompileObject(compiler, defopts, srcfile, dstfile))
objects.append(dstfile)
defopts.objfiles.update(objects)
del objects
defopts.pic = False
defopts.defines.clear()
if len(target.libs) > 0:
for library in target.libs:
libtgt = find_target_by_name(library.value)
libprj = find_project_by_target(libtgt.name)
if libtgt.type == Output.LIB:
defopts.libdirs.add(libprj.destination.libpath)
defopts.archives.add(library.value)
elif libtgt.type == Output.DLL:
defopts.objfiles.add(libprj.destination.dll(library.value))
if len(defopts.objfiles) == 0:
return
command: Command
match target.type:
case Output.EXE:
outfile = project.destination.bin(target.name)
command = CompileProgram(compiler, defopts, outfile)
case Output.LIB:
outfile = project.destination.lib(target.name)
command = CompileArchive(compiler, defopts, outfile)
case Output.DLL:
outfile = project.destination.dll(target.name)
command = CompileDynlib(compiler, defopts, outfile)
case Output.OBJ:
assert False, "Not Implemented"
commands.append(command)
+84 -5
View File
@@ -2,33 +2,82 @@ from abc import ABC
from abc import abstractmethod from abc import abstractmethod
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass from dataclasses import dataclass
from logging import getLogger
from logging import Logger
from pathlib import Path from pathlib import Path
from typing import override from typing import override
from ..compiler import Compiler from ..compiler import Compiler
from ..compiler import CompilerOptions from ..compiler import CompilerOptions
from ..models import Result from ..models import Result
from ..shell import test
_logger = getLogger("chinook")
@dataclass @dataclass
class Command(ABC): class Command(ABC):
compiler: Compiler compiler: Compiler
options: CompilerOptions options: CompilerOptions
logger: Logger
def __post_init__(self) -> None: def __post_init__(self) -> None:
if self.logger == None:
self.logger = _logger
self.options = deepcopy(self.options) self.options = deepcopy(self.options)
def execute(
self,
percentage: float,
reason: str
) -> Result | None:
if reason != "Testing":
return self._execute_building(percentage)
else:
return self._execute_testing()
@abstractmethod @abstractmethod
def execute(self) -> Result: def _execute(self) -> Result:
... ...
def _execute_building(self, percentage: float) -> Result | None:
message = f"[{percentage:.0%}] "
try:
result = self._execute()
if result.success:
message += f"{result.action} {result.target}"
self.logger.info(message)
else:
message += f"Failed {result.target} because "
for index in range(len(result.diagnostics) - 1):
message += f"{result.diagnostics[index]} "
message += result.diagnostics[-1]
self.logger.error(message)
return result
except Exception as error:
message += f"Failed {result.target} because {str(error)}"
self.logger.error(message)
return None
def _execute_testing(self) -> Result | None:
try:
result = self._execute()
message = f"{result.action} {result.target}"
message += " - Passed" if result.retcode == 0 else " - Failed"
self.logger.info(message)
return result
except Exception as error:
message = f"Failed {result.target} because {str(error)}"
self.logger.error(message)
return None
@dataclass @dataclass
class CompileProgram(Command): class CompileProgram(Command):
exefile: Path exefile: Path
@override @override
def execute(self) -> Result: def _execute(self) -> Result:
return self.compiler.compile_program( return self.compiler.compile_program(
self.options, self.options,
self.exefile self.exefile
@@ -39,7 +88,7 @@ class CompileArchive(Command):
libfile: Path libfile: Path
@override @override
def execute(self) -> Result: def _execute(self) -> Result:
return self.compiler.compile_archive( return self.compiler.compile_archive(
self.options, self.options,
self.libfile self.libfile
@@ -50,7 +99,7 @@ class CompileDynlib(Command):
dynfile: Path dynfile: Path
@override @override
def execute(self) -> Result: def _execute(self) -> Result:
return self.compiler.compile_library( return self.compiler.compile_library(
self.options, self.options,
self.dynfile self.dynfile
@@ -62,9 +111,39 @@ class CompileObject(Command):
dstfile: Path dstfile: Path
@override @override
def execute(self) -> Result: def _execute(self) -> Result:
return self.compiler.compile_object( return self.compiler.compile_object(
self.options, self.options,
self.srcfile, self.srcfile,
self.dstfile self.dstfile
) )
@dataclass
class ExecuteTest(Command):
testfile: Path
@override
def _execute(self) -> Result:
abspath = Path.absolute(self.testfile)
result = Result(
success = False,
action = "Testing",
target = str(self.testfile),
command = "",
retcode = -1
)
def handle_success(retcode: int, cmdstr: str, stdout: str) -> None:
result.success = True
result.command = cmdstr
result.retcode = retcode
result.diagnostics.append(stdout)
def handle_failure(retcode: int, cmdstr: str, stderr: str) -> None:
result.success = False
result.command = cmdstr
result.retcode = retcode
result.diagnostics.append(stderr)
test(abspath, handle_success, handle_failure)
return result
+9
View File
@@ -9,3 +9,12 @@ class NoTargetsFoundError(Exception):
def __init__(self, project: Project) -> None: def __init__(self, project: Project) -> None:
super().__init__(f"No targets for project: {project.cid}") super().__init__(f"No targets for project: {project.cid}")
self._project = project self._project = project
class NoTestsFoundError(Exception):
@property
def project(self) -> Project:
return self._project
def __init__(self, project: Project) -> None:
super().__init__(f"No tests for project: {project.cid}")
self._project = project
+17
View File
@@ -0,0 +1,17 @@
from ..models import Result
from ._commands import Command
def execute(commands: list[Command], reason: str) -> list[Result]:
results: list[Result] = []
cmdcount: int = len(commands)
if cmdcount == 1:
results.append(commands[0].execute(1, reason))
return results
executed: int = 0
for command in commands:
results.append(command.execute(executed / cmdcount, reason))
executed += 1
return results
+80
View File
@@ -0,0 +1,80 @@
from logging import getLogger
from logging import Logger
from pathlib import Path
from ..compiler import Compiler
from ..compiler import CompilerOptions
from ..management import latest
from ..management import find_target_by_name
from ..management import find_project_by_target
from ..models import Output
from ..models import Project
from ..models import Target
from .._config import Config
from ._commands import Command
from ._commands import CompileArchive
from ._commands import CompileDynlib
from ._commands import CompileObject
from ._commands import CompileProgram
_logger = getLogger("chinook")
def prepare(
appcfg: Config,
project: Project,
target: Target,
logger: Logger = _logger
) -> list[Command]:
objects: list[Path] = []
commands: list[Command] = []
# TODO: clean this up.
compiler = Compiler(appcfg.native_tooling, logger)
defopts = CompilerOptions()
defopts.pic = target.type == Output.DLL
defopts.incdirs.update([v.value for v in target.incs.includes])
defopts.defines.update([v.value for v in target.defs])
defopts.flags.update([v.value for v in target.opts])
for file in target.srcs:
srcfile = project.origination.src(file.value)
dstfile = project.destination.obj(file.value)
if latest(srcfile, dstfile):
continue
commands.append(CompileObject(compiler, defopts, logger, srcfile, dstfile))
objects.append(dstfile)
defopts.objfiles.update(objects)
del objects
defopts.pic = False
defopts.defines.clear()
if len(target.libs) > 0:
for library in target.libs:
libtgt = find_target_by_name(library.value)
libprj = find_project_by_target(libtgt.name)
if libtgt.type == Output.LIB:
defopts.libdirs.add(libprj.destination.libpath)
defopts.archives.add(library.value)
elif libtgt.type == Output.DLL:
defopts.objfiles.add(libprj.destination.dll(library.value))
if len(defopts.objfiles) == 0:
return commands
command: Command
match target.type:
case Output.EXE:
outfile = project.destination.bin(target.name)
command = CompileProgram(compiler, defopts, logger, outfile)
case Output.LIB:
outfile = project.destination.lib(target.name)
command = CompileArchive(compiler, defopts, logger, outfile)
case Output.DLL:
outfile = project.destination.dll(target.name)
command = CompileDynlib(compiler, defopts, logger, outfile)
case Output.OBJ:
assert False, "Not Implemented"
commands.append(command)
return commands
+34
View File
@@ -0,0 +1,34 @@
from logging import getLogger
from logging import Logger
from ..models import Project
from ..models import Result
from .._config import Config
from ._commands import Command
from ._commands import ExecuteTest
from ._except import NoTestsFoundError
from ._execute import execute
from ._prepare import prepare
_logger = getLogger("chinook")
def test(
appcfg: Config,
project: Project,
logger: Logger = _logger
) -> list[Result]:
if len(project.tests) == 0:
raise NoTestsFoundError(project)
commands: list[Command] = []
for target in project.tests:
commands.extend(prepare(appcfg, project, target, logger))
results = execute(commands, "Building")
commands.clear()
for target in project.tests:
testfile = project.destination.bin(target.name)
commands.append(ExecuteTest(None, None, logger, testfile))
results.extend(execute(commands, "Testing"))
return results
+1
View File
@@ -1 +1,2 @@
from ._execute import execute from ._execute import execute
from ._execute import test
+34 -6
View File
@@ -4,8 +4,8 @@ from subprocess import run
from typing import Callable from typing import Callable
from typing import Optional from typing import Optional
DataCallback = Callable[[str, str, str], None] DataCallback = Callable[[int, str, str], None]
ErrorCallback = Callable[[str, str, str], None] ErrorCallback = Callable[[int, str, str], None]
def execute( def execute(
@@ -24,14 +24,42 @@ def execute(
if on_data != None: if on_data != None:
on_data( on_data(
command, result.returncode,
result.stdout.decode("utf-8"), result.args,
result.stderr.decode("utf-8") result.stdout.decode("utf-8")
) )
except CalledProcessError as error: except CalledProcessError as error:
if on_error != None: if on_error != None:
on_error( on_error(
error.returncode,
error.cmd,
error.stderr.decode("utf-8")
)
def test(
command: str,
on_data: Optional[DataCallback],
on_error: Optional[ErrorCallback]
) -> None:
try:
result = run(
command,
shell = False,
check = False,
stdout = PIPE,
stderr = PIPE
)
if on_data != None:
on_data(
result.returncode,
result.args,
result.stdout.decode("utf-8")
)
except CalledProcessError as error:
if on_error != None:
on_error(
error.returncode,
error.cmd, error.cmd,
error.stdout.decode("utf-8"),
error.stderr.decode("utf-8") error.stderr.decode("utf-8")
) )
+9
View File
@@ -0,0 +1,9 @@
name: testing
gpid: examples
semv: 1.0.0
tests:
- name: factorial_test
type: program
srcs:
- ./entry.cpp
+13
View File
@@ -0,0 +1,13 @@
int factorial(int start) {
if (start < 0)
return 0;
auto result = 1;
for (auto index = 2; index < start; index++)
result *= index;
return result;
}
int main(int argc, char** argv) {
return factorial(5) == 120;
}