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
+44 -5
View File
@@ -20,14 +20,42 @@ def _build(args: Namespace) -> None:
try:
projpath = Path(args.project)
cfgpath = projpath / "chinookconfig"
return pipeline.build(
Config.create_default_or_load(cfgpath),
management.load(projpath)
)
appcfg = Config.create_default_or_load(cfgpath)
projcfg = management.load(projpath)
result = pipeline.build(appcfg, projcfg, logger)
logger.info(f"[100%] Built {projcfg.cid}")
return result
except Exception as err:
logger.exception(str(err))
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:
print("\n".join([
@@ -50,7 +78,18 @@ def main() -> None:
nargs="?",
default=".",
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"
)
+13 -5
View File
@@ -1,7 +1,8 @@
from dataclasses import dataclass
from dataclasses import field
from pathlib import Path
from logging import getLogger
from logging import Logger
from pathlib import Path
from ..models import Abi
from ..models import Result
@@ -13,6 +14,8 @@ from ._mingw import MingwDriver
from ._msvc import MsvcDriver
from ._options import CompilerOptions
_logger = getLogger("chinook")
@dataclass
class Compiler:
@@ -25,10 +28,11 @@ class Compiler:
}
tooling: Tooling
logger: Logger = field(default=_logger)
_driver: CompilerDriver = field(init=False)
def __post_init__(self) -> None:
self._driver = Compiler._driver_for(self.tooling)
self._driver = Compiler._driver_for(self.tooling, self.logger)
def compile_program(
self,
@@ -60,8 +64,12 @@ class Compiler:
return self._driver.compile_object(options, srcpath, dstPath)
@classmethod
def _driver_for(cls, tooling: Tooling) -> CompilerDriver:
def _driver_for(
cls,
tooling: Tooling,
logger: Logger
) -> CompilerDriver:
from ..models import Triple
triples = tooling.when or [Triple.current()]
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 abstractmethod
from abc import ABC
from abc import abstractmethod
from logging import getLogger
from logging import Logger
from pathlib import Path
from ..models import Tooling
from ..models import Result
from ._options import CompilerOptions
_logger = getLogger("chinook")
class CompilerDriver(ABC):
def __init__(self, tooling: Tooling) -> None:
def __init__(
self,
tooling: Tooling,
logger: Logger = _logger
) -> None:
if tooling is None:
raise ValueError
self.tooling = tooling
self.logger = logger
@abstractmethod
def compile_program(
+86 -67
View File
@@ -2,10 +2,7 @@ from pathlib import Path
from ..models import Optimization
from ..models import Result
from ..models import StdC
from ..models import StdCpp
from ..shell import execute
from .._logger import logger
from ._driver import CompilerDriver
from ._options import CompilerOptions
@@ -48,17 +45,30 @@ class GnucDriver(CompilerDriver):
for directory in options.libdirs: args.append(f"-L{str(directory)}")
for archive in options.archives: args.append(f"-l{archive}")
cmdstr = ' '.join(args)
# logger.info(cmdstr)
# self.logger.info(cmdstr)
result: list[Result] = [None]
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)
result = Result(
success = False,
action = "Linked",
target = str(dstpath),
command = cmdstr,
retcode = -1
)
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(
self,
@@ -70,15 +80,28 @@ class GnucDriver(CompilerDriver):
args.append(str(obj))
cmdstr = ' '.join(args)
result: list[Result] = [None]
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)
result = Result(
success = False,
action = "Linked",
target = str(dstpath),
command = cmdstr,
retcode = -1
)
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(
self,
@@ -106,17 +129,30 @@ class GnucDriver(CompilerDriver):
for directory in options.libdirs: args.append(f"-L{str(directory)}")
for archive in options.archives: args.append(f"-l{archive}")
cmdstr = ' '.join(args)
# logger.info(cmdstr)
# self.logger.info(cmdstr)
result: list[Result] = [None]
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)
result = Result(
success = False,
action = "Linked",
target = str(dstpath),
command = cmdstr,
retcode = -1
)
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(
self,
@@ -150,44 +186,27 @@ class GnucDriver(CompilerDriver):
for archive in options.archives: args.append(f"-l{archive}")
for define in options.defines: args.append(f"-D{define}")
cmdstr = ' '.join(args)
# logger.info(cmdstr)
# self.logger.info(cmdstr)
result: list[Result] = [None]
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(
result = Result(
success = False,
target = target,
command = cmdstr
action = "Compiled",
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 Target
from ._clone import clone
from ._except import NoProjectFoundError
from ._latest import latest
from ._load import load
+7
View File
@@ -32,6 +32,7 @@ def load(path: Path) -> Project:
project = Project.from_dict(yamlobj)
project.origination = Origination(path)
project.destination = Destination(path)
_collect_profiles(project)
_collect_required(project)
_collect_targets(project)
_flatten_targets(project)
@@ -39,6 +40,12 @@ def load(path: Path) -> Project:
except Exception as 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:
for required in project.requires:
_clone_then_load_required(required)
+6 -2
View File
@@ -58,8 +58,12 @@ class Dependency(yaml.YAMLError):
return cls(**data)
@classmethod
def from_list(cls, data: list[str]) -> list[Self]:
return [cls.from_dict(v) for v in data]
def from_list(cls, data: list[dict | Self]) -> list[Self]:
return [
v if isinstance(v, Dependency) \
else cls.from_dict(v)
for v in data
]
@classmethod
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 ._identity import Identity
from ._origination import Origination
from ._profile import Profile
from ._target import Target
@@ -18,8 +19,10 @@ class Project(Identity):
yaml_loader: ClassVar[type[yaml.SafeLoader]] = yaml.SafeLoader
yaml_tag: ClassVar[str] = u"!project"
profiles: list[Profile] = field(default_factory=list)
requires: list[Dependency] = 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)
origination: Origination = field(default=Origination.cwd())
@@ -28,8 +31,10 @@ class Project(Identity):
@classmethod
def to_dict(cls, data: Self) -> dict:
return {
"profiles": Profile.to_list(data.profiles),
"requires": Dependency.to_list(data.requires),
"targets": Target.to_list(data.targets),
"tests": Target.to_list(data.targets),
"exports": data.exports
}
@@ -53,8 +58,10 @@ class Project(Identity):
"name": data.get("name") or bad_attr("name", data),
"gpid": data.get("gpid") or bad_attr("gpid", data),
"semv": data.get("semv") or bad_attr("semv", data),
"profiles": Profile.from_list(data.get("profiles", [])),
"requires": Dependency.from_list(data.get("requires", [])),
"targets": Target.from_list(data.get("targets", [])),
"tests": Target.from_list(data.get("tests", [])),
"exports": data.get("exports", [])
})
@@ -67,3 +74,13 @@ class Project(Identity):
return cls.from_dict(
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
class Result:
success: bool
action: str
target: str
command: str
retcode: int
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:
return cls(**{
**data,
"when": [
v if isinstance(v, Triple) \
else Triple.from_string(v)
for v in data.get("when", [])
]
"when": Triple.from_list(data.get("when", []))
})
@classmethod
def from_list(cls, data: list[dict]) -> list[Self]:
def from_list(cls, data: list[dict | Self]) -> list[Self]:
return [
v if isinstance(v, Tooling) \
else Tooling.from_dict(v)
else cls.from_dict(v)
for v in data
]
+24 -14
View File
@@ -24,7 +24,7 @@ class Arch(enum.StrEnum):
S390X = "s390x"
WASM32 = "wasm32"
WASM64 = "wasm64"
UNKNOWN = "unknown"
ANY = "any"
class Sys(enum.StrEnum):
LINUX = "linux"
@@ -36,8 +36,7 @@ class Sys(enum.StrEnum):
ANDROID = "android"
IOS = "ios"
WASI = "wasi"
NONE = "none"
UNKNOWN = "unknown"
ANY = "any"
class Abi(enum.StrEnum):
GNU = "gnu"
@@ -45,8 +44,7 @@ class Abi(enum.StrEnum):
MINGW = "mingw"
ANDROID = "android"
ELF = "elf"
NONE = "none"
UNKNOWN = "unknown"
ANY = "any"
@dataclass
@@ -91,9 +89,9 @@ class Triple(yaml.YAMLObject):
def native(self) -> bool:
host = Triple.current()
return (
(self.arch == Arch.UNKNOWN or self.arch == host.arch) and
(self.sys == Sys.UNKNOWN or self.sys == host.sys) and
(self.abi == Abi.UNKNOWN or self.abi == host.abi)
(self.arch == Arch.ANY or self.arch == host.arch) and
(self.sys == Sys.ANY or self.sys == host.sys) and
(self.abi == Abi.ANY or self.abi == host.abi)
)
def __str__(self) -> str:
@@ -111,6 +109,10 @@ class Triple(yaml.YAMLObject):
def to_string(cls, triple: Self) -> str:
return str(triple)
@classmethod
def to_list(cls, data: list[Self]) -> list[str]:
return [cls.to_string(v) for v in data]
@classmethod
def to_yaml(
cls,
@@ -141,11 +143,19 @@ class Triple(yaml.YAMLObject):
return fallback
return cls(
arch = _coerce(Arch, parts[0].lower(), Arch.UNKNOWN),
sys = _coerce(Sys, parts[1].lower(), Sys.UNKNOWN),
abi = _coerce(Abi, parts[2].lower(), Abi.UNKNOWN)
arch = _coerce(Arch, parts[0].lower(), Arch.ANY),
sys = _coerce(Sys, parts[1].lower(), Sys.ANY),
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
def from_yaml(
cls,
@@ -157,18 +167,18 @@ class Triple(yaml.YAMLObject):
@classmethod
def _select_arch(cls) -> Arch:
curr = platform.machine().lower()
return cls.__arch_map.get(curr, Arch.UNKNOWN)
return cls.__arch_map.get(curr, Arch.ANY)
@classmethod
def _select_sys(cls) -> Sys:
curr = platform.system().lower()
return cls.__sys_map.get(curr, Sys.UNKNOWN) \
return cls.__sys_map.get(curr, Sys.ANY) \
if system.platform != "android" \
else Sys.ANDROID
@classmethod
def _select_abi(cls, arch: Arch, sys: Sys) -> Abi:
res = Abi.UNKNOWN
res = Abi.ANY
if sys == Sys.WINDOWS:
res = Abi.MSVC
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 ..compiler import CompilerOptions
from ..management import latest
from ..management import find_target_by_name
from ..management import find_project_by_target
from ..models import Access
from ..models import Accessor
from ..models import Output
from ..models import Project
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
from ..models import Project
from ..models import Result
from .._config import Config
from ._commands import Command
from ._except import NoTargetsFoundError
from ._execute import execute
from ._prepare import prepare
_logger = getLogger("chinook")
def build(appcfg: Config, project: Project) -> list[Result]:
def build(
appcfg: Config,
project: Project,
logger: Logger = _logger
) -> list[Result]:
if len(project.targets) == 0:
raise NoTargetsFoundError(project)
# Put together list of commands and then execute them.
commands: list[Command] = []
results: list[Result] = []
for target in project.targets:
_prepare_build_commands(appcfg, project, target, commands)
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)
commands.extend(prepare(appcfg, project, target, logger))
return execute(commands, "Building")
+84 -5
View File
@@ -2,33 +2,82 @@ from abc import ABC
from abc import abstractmethod
from copy import deepcopy
from dataclasses import dataclass
from logging import getLogger
from logging import Logger
from pathlib import Path
from typing import override
from ..compiler import Compiler
from ..compiler import CompilerOptions
from ..models import Result
from ..shell import test
_logger = getLogger("chinook")
@dataclass
class Command(ABC):
compiler: Compiler
options: CompilerOptions
logger: Logger
def __post_init__(self) -> None:
if self.logger == None:
self.logger = _logger
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
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
class CompileProgram(Command):
exefile: Path
@override
def execute(self) -> Result:
def _execute(self) -> Result:
return self.compiler.compile_program(
self.options,
self.exefile
@@ -39,7 +88,7 @@ class CompileArchive(Command):
libfile: Path
@override
def execute(self) -> Result:
def _execute(self) -> Result:
return self.compiler.compile_archive(
self.options,
self.libfile
@@ -50,7 +99,7 @@ class CompileDynlib(Command):
dynfile: Path
@override
def execute(self) -> Result:
def _execute(self) -> Result:
return self.compiler.compile_library(
self.options,
self.dynfile
@@ -62,9 +111,39 @@ class CompileObject(Command):
dstfile: Path
@override
def execute(self) -> Result:
def _execute(self) -> Result:
return self.compiler.compile_object(
self.options,
self.srcfile,
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:
super().__init__(f"No targets for project: {project.cid}")
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 test
+34 -6
View File
@@ -4,8 +4,8 @@ from subprocess import run
from typing import Callable
from typing import Optional
DataCallback = Callable[[str, str, str], None]
ErrorCallback = Callable[[str, str, str], None]
DataCallback = Callable[[int, str, str], None]
ErrorCallback = Callable[[int, str, str], None]
def execute(
@@ -24,14 +24,42 @@ def execute(
if on_data != None:
on_data(
command,
result.stdout.decode("utf-8"),
result.stderr.decode("utf-8")
result.returncode,
result.args,
result.stdout.decode("utf-8")
)
except CalledProcessError as error:
if on_error != None:
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.stdout.decode("utf-8"),
error.stderr.decode("utf-8")
)