diff --git a/README.md b/README.md index 78610889..9b220acc 100644 --- a/README.md +++ b/README.md @@ -118,3 +118,51 @@ each one of the above-mentioned algorithms. In addition, there are tests that have either multiple inputs and / or multiple outputs (like `ExampleFunctionalProducerMultiple`) that can be used as a template for the more typical case when working with multiple inputs or outputs. + +### Generating boilerplate with generateFunctional + +`k4FWCore/helpers/generateFunctional` is a code generator that produces the C++ +boilerplate for a new functional algorithm. The functional type (Consumer, +Producer, Transformer, MultiTransformer, FilterPredicate) is inferred +automatically from the number of inputs and outputs, or can be set explicitly. + +Requirements: Python ≥ 3.9 and [jinja2](https://pypi.org/project/Jinja2/). +With [uv](https://github.com/astral-sh/uv) installed, dependencies are +resolved automatically via the PEP 723 script block. + +```bash +# Producer with one output collection and one property +python3 k4FWCore/helpers/generateFunctional MyProducer \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + -p 'int:ExampleInt:3:An example integer property' + +# Transformer (inferred from 1 input + 1 output) +python3 k4FWCore/helpers/generateFunctional MyTransformer \ + -i 'edm4hep::MCParticleCollection:InputCollection' \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + --private-properties + +# MultiTransformer with type aliases +python3 k4FWCore/helpers/generateFunctional MyMulti \ + -i 'edm4hep::MCParticleCollection:Particles' \ + 'edm4hep::TrackCollection:Tracks' \ + -o 'edm4hep::MCParticleCollection:NewParticles' \ + 'podio::UserDataCollection:Counter' \ + --type-aliases + +# FilterPredicate (type must be specified explicitly) +python3 k4FWCore/helpers/generateFunctional MyFilter filter \ + -i 'edm4hep::MCParticleCollection:InputCollection' + +# Consumer with runtime (variable-length) input collections +python3 k4FWCore/helpers/generateFunctional MyConsumer \ + -i 'edm4hep::MCParticleCollection:Inputs' \ + --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' + +# Also emit a CMakeLists.txt skeleton +python3 k4FWCore/helpers/generateFunctional MyProducer \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + --cmake +``` + +Run `python3 k4FWCore/helpers/generateFunctional --help` for the full list of options. diff --git a/k4FWCore/CMakeLists.txt b/k4FWCore/CMakeLists.txt index d19c08e5..f57807d4 100644 --- a/k4FWCore/CMakeLists.txt +++ b/k4FWCore/CMakeLists.txt @@ -18,6 +18,12 @@ limitations under the License. ]] gaudi_install(SCRIPTS) +install(PROGRAMS helpers/generateFunctional + DESTINATION ${CMAKE_INSTALL_BINDIR}) + +if(BUILD_TESTING) + add_subdirectory(helpers/tests) +endif() gaudi_add_library(k4FWCore SOURCES src/KeepDropSwitch.cpp diff --git a/k4FWCore/helpers/AGENT.md b/k4FWCore/helpers/AGENT.md new file mode 100644 index 00000000..decc19c5 --- /dev/null +++ b/k4FWCore/helpers/AGENT.md @@ -0,0 +1,180 @@ + +# AGENT.md — k4FWCore/helpers + +Context for AI agents working on `generateFunctional` and its test suite. + +--- + +## What this directory contains + +| File/Dir | Purpose | +|---|---| +| `generateFunctional` | Code generator: produces Gaudi Functional C++ boilerplate from CLI arguments | +| `README.md` | Full user-facing reference (arguments, examples, exit codes) | +| `tests/` | Bash build-tests: generate → cmake → build for each algorithm type | +| `tests/_test_common.sh` | Shared helpers sourced by every `test_*.sh` | +| `tests/run_all_tests.sh` | Runs all `test_*.sh` and reports a pass/fail summary | + +--- + +## Architecture of generateFunctional + +The script is intentionally structured so that all string parsing happens once at the CLI boundary and never again: + +``` +CLI args + └─ _build_spec() Parses strings → AlgorithmSpec dataclass + └─ generate() AlgorithmSpec → (cpp_source, cmake_source) + ├─ _build_includes() + ├─ _build_constructor() k4FWCore style (with brace rules below) + ├─ _build_constructor_gaudi() + ├─ _build_op_signature() + ├─ _build_op_body() + └─ Jinja2 template (_CPP_TEMPLATE, _CMAKE_TEMPLATE) +``` + +### Key data classes + +- **`DataSpec`** — one input or output collection: `type_name`, `key`, `is_vector`. + - `edm4hep_header` property returns `TypeCollection.h` (keep "Collection" in filename — a past bug stripped it). + - `_default_key()` derives a key from the type name by stripping namespace and `Collection`, e.g. `edm4hep::MCParticleCollection` → `MCParticles`. + +- **`RuntimeInputSpec`** — a `DataSpec` with additional default location names for `KeyValues`. + +- **`PropertySpec`** — a `Gaudi::Property` member. + - `member_name` lowercases the first character after `m_`: `Offset` → `m_offset`, not `m_Offset`. + +- **`AlgorithmSpec`** — the single object passed through all generation functions. Contains all inputs, outputs, options, and derived properties used by templates. + +### Constructor brace rules + +This is the trickiest part of code generation. The k4FWCore constructors follow different conventions per type: + +| Type | Inputs | Outputs | +|---|---|---| +| `Consumer` / `FilterPredicate` | bare for single: `KeyValue(...)` | — | +| `Consumer` / `FilterPredicate` | braced for multiple: `{KeyValue(...), ...}` | — | +| `Producer` | `{}` (always empty) | bare for single: `KeyValue(...)` | +| `Producer` | `{}` | braced for multiple: `{KeyValues(...), ...}` | +| `Transformer` / `MultiTransformer` | **always braced**: `{KeyValue(...)}` | **always braced**: `{KeyValue(...)}` | + +In `_build_constructor`, `_brace_block()` always wraps in `{}` (used for transformer/producer), while `_bare_block()` leaves a single item unwrapped (used for consumer/filter). + +### Template structure (_CPP_TEMPLATE) + +Order of sections in the generated `.cpp`: + +1. `// Generated by ...` header comment with full command line +2. `#include` directives +3. Optional `using BaseClass_t = ...` (Gaudi framework only) +4. Optional `using retType = std::tuple<...>` (k4FWCore multi-output) +5. Optional `using XxxColl = ...` type aliases (`--type-aliases`) +6. Optional `namespace X {` +7. Class definition: + - Constructor + - `StatusCode initialize()` (only when vector inputs are present) + - `operator()` + - `StatusCode finalize()` (only with `--event-context`) — **must be before `private:`** + - Optional `private:` label (when `--private-properties` or `--event-context`) + - Properties + - `mutable std::set m_eventNumbersSeen{}` and `m_mutex` (only with `--event-context`) +8. `DECLARE_COMPONENT(ClassName)` + +### Functional type inference + +``` +inputs > 0, outputs == 0 → consumer +inputs == 0, outputs > 0 → producer +inputs > 0, outputs == 1 → transformer +inputs > 0, outputs > 1 → multitransformer +filter → never inferred; must be explicit +``` + +`transformer` auto-promotes to `multitransformer` when multiple `--outputs` are given. + +--- + +## Known remaining gaps vs. test examples + +These are design limitations, not bugs: + +1. **`KeyValue` default location = key name.** The script emits `KeyValue("OutputCollection", "OutputCollection")` but test examples have `KeyValue("OutputCollection", "MCParticles")`. There is no CLI argument for a separate default location value. + +2. **Include order.** Script: `k4FWCore/` first, then `Gaudi/Property.h`, then `edm4hep/`. Test examples: `Gaudi/Property.h` first, then `edm4hep/`, then `k4FWCore/`. + +3. **Multi-transformer output aliases.** Test examples define individual `using Counter = ...; using Particle = ...;` aliases for each output type. The script emits a single `using retType = std::tuple<...>` with raw types. + +4. **No license header.** The script emits `// Generated by ...`; test examples carry the Apache 2.0 block. + +--- + +## Test scripts + +Each script in `tests/` covers one feature axis: + +| Script | Feature | +|---|---| +| `test_producer.sh` | Single output, property | +| `test_consumer.sh` | Single input, property | +| `test_transformer.sh` | Single in/out, `--private-properties` | +| `test_multitransformer.sh` | Multiple in/out, `--type-aliases`, `podio::UserDataCollection` | +| `test_filter.sh` | `FilterPredicate` | +| `test_runtime_consumer.sh` | `--runtime-inputs` / `KeyValues` vector input | +| `test_runtime_transformer.sh` | `--runtime-outputs` / `std::vector` return | +| `test_event_context.sh` | `--event-context`, `finalize()` placement | +| `test_gaudi_framework.sh` | `--framework gaudi`, `--namespace` | + +Each script sources `_test_common.sh` which: +- Finds `generateFunctional` (installed on `PATH` first, then `../generateFunctional` fallback) +- Creates a `mktemp -d` sandbox, cleaned up on `EXIT` +- Provides `run_cmake_build [args...]` that runs generate → cmake configure → cmake build + +Tests require a Key4hep environment (`k4FWCore`, `EDM4HEP`, `Gaudi` on `CMAKE_PREFIX_PATH`). Source the Key4hep setup before running: + +```bash +source /cvmfs/sw.hsf.org/key4hep/setup.sh +bash k4FWCore/helpers/tests/run_all_tests.sh +``` + +--- + +## Installation + +`generateFunctional` is installed to `CMAKE_INSTALL_BINDIR` via `k4FWCore/CMakeLists.txt`: + +```cmake +install(PROGRAMS helpers/generateFunctional + DESTINATION ${CMAKE_INSTALL_BINDIR}) +``` + +After `cmake --install`, `generateFunctional` is on `PATH` in the Key4hep environment. + +--- + +## Common mistakes to avoid + +- **Do not strip `Collection` from edm4hep header filenames.** `edm4hep::MCParticleCollection` → `edm4hep/MCParticleCollection.h`, not `edm4hep/MCParticle.h`. See `DataSpec.edm4hep_header`. +- **Do not wrap `Consumer`/`FilterPredicate` single inputs in braces.** Only `Transformer`/`Producer` use `_brace_block()`. +- **`finalize()` must be emitted before `private:`.** The Jinja2 template places `finalize()` in its own block before the `{% if spec.private_props or spec.event_context %}private:{% endif %}` block. +- **Property member names must be lowercase after `m_`.** `PropertySpec.member_name` lowercases `n[0]`; do not change this or generated names diverge from k4FWCore conventions. +- **`--runtime-outputs` is k4FWCore-only.** The parser enforces this, but the cmake template only adds podio explicitly for `--framework gaudi`; for k4fwcore it is a transitive dependency of `k4FWCore::k4FWCore`. +- **Do not link `Gaudi::GaudiAlgLib` for `--framework gaudi`.** This target was removed in Gaudi 40.x. The cmake template links only `Gaudi::GaudiKernel`. +- **Native Gaudi constructor takes separate input and output arguments, not a single merged list.** `_build_constructor_gaudi` passes `_arg(in_kvs), _arg(out_kvs)` as separate arguments. A single KV is bare; multiple KVs are `{kv1, kv2, ...}`. +- **`DECLARE_COMPONENT` must use the fully qualified name when `--namespace` is set.** The template emits `DECLARE_COMPONENT(Ns::ClassName)` outside the namespace block. diff --git a/k4FWCore/helpers/AGENT_USAGE.md b/k4FWCore/helpers/AGENT_USAGE.md new file mode 100644 index 00000000..88b20b66 --- /dev/null +++ b/k4FWCore/helpers/AGENT_USAGE.md @@ -0,0 +1,125 @@ + +# Using an AI Agent with generateFunctional + +You can ask an AI agent (such as Claude in Cowork or via the API) to run +`generateFunctional` for you. Instead of memorising flags, describe your +algorithm in plain language and the agent handles the rest. + +--- + +## How it works + +1. You describe the algorithm you need. +2. The agent translates your description into a `generateFunctional` command. +3. The agent runs the command and shows you the generated `.cpp` (and + optionally `CMakeLists.txt`). +4. You ask for changes; the agent re-runs with updated flags. + +--- + +## What to tell the agent + +The more detail you provide, the closer the first attempt will be to what you +want. Cover these points: + +| What | Example | +|---|---| +| **Algorithm name** | `MyParticleSelector` | +| **Inputs** — type and key name | `edm4hep::MCParticleCollection` named `InputParticles` | +| **Outputs** — type and key name | `edm4hep::MCParticleCollection` named `SelectedParticles` | +| **Properties** — C++ type, name, default, description | `float` named `MinPt`, default `0.5`, "Minimum transverse momentum" | +| **Private properties?** | Yes / No | +| **EventContext needed?** | Yes / No | +| **Type aliases?** | Yes / No | +| **CMake file too?** | Yes / No | +| **Framework** | `k4fwcore` (default) or `gaudi` | +| **C++ namespace** | e.g. `MyExperiment` | + +You do not need to know any flags — just describe what you want. + +--- + +## Example prompts + +### Minimal — let the agent fill in the gaps + +> Generate a transformer called `TrackFilter` that reads +> `edm4hep::TrackCollection` and writes a filtered +> `edm4hep::TrackCollection`. + +### With properties + +> Generate a transformer `EnergyThresholdFilter` that takes +> `edm4hep::MCParticleCollection:InputParticles` as input and returns +> `edm4hep::MCParticleCollection:OutputParticles`. Add a float property +> `MinEnergy` with default `1.0` and description "Minimum particle energy in +> GeV". Put properties under `private:`. Also emit a `CMakeLists.txt`. + +### Multiple inputs and outputs + +> I need a MultiTransformer `JetBuilder` with two inputs — +> `edm4hep::MCParticleCollection:Particles` and +> `edm4hep::TrackCollection:Tracks` — and two outputs — +> `edm4hep::ReconstructedParticleCollection:Jets` and +> `podio::UserDataCollection:JetPt`. Use type aliases. + +### Runtime (variable-length) inputs + +> Generate a consumer `MultiCollectionReader` that reads a variable number +> of `edm4hep::MCParticleCollection` inputs at runtime, with default names +> `MCParticles0` and `MCParticles1`. + +### From an existing example + +> Look at `ExampleFunctionalTransformerRuntimeCollections.cpp` in the test +> folder and generate something similar for `edm4hep::TrackCollection`. + +### Refinement after seeing the output + +> That looks good. Can you add an `int` property `MaxParticles` with default +> `100`, and regenerate with `--force`? + +--- + +## What the agent can do automatically + +- Infer the functional type (`Consumer`, `Producer`, `Transformer`, + `MultiTransformer`) from your inputs and outputs. +- Derive default key names from collection types when you don't specify them + (e.g. `edm4hep::MCParticleCollection` → key `MCParticles`). +- Add the correct `#include` directives for all edm4hep and podio types. +- Emit `DECLARE_COMPONENT()` and a ready-to-build `CMakeLists.txt`. +- Re-run with `--force` to overwrite after you request changes. + +--- + +## Tips + +- **`filter` must be explicit.** The agent cannot infer `FilterPredicate` from + I/O counts alone — say "FilterPredicate" or "filter type" in your prompt. +- **Key names matter.** If your steering file already names the collections, + tell the agent the exact keys so the generated `KeyValue` strings match. +- **Iterate freely.** Generated code is cheap to redo. Ask the agent to tweak + property types, add an `EventContext`, switch to `--use-class`, or change + the namespace — it will re-run the generator rather than hand-editing the + output. +- **Review before committing.** Check the generated constructor argument order + and `operator()` signature against your project's conventions before adding + the file to git. diff --git a/k4FWCore/helpers/README.md b/k4FWCore/helpers/README.md new file mode 100644 index 00000000..1bf0ca58 --- /dev/null +++ b/k4FWCore/helpers/README.md @@ -0,0 +1,284 @@ + +# generateFunctional — Gaudi Functional C++ Class Generator + +`generateFunctional` writes the boilerplate for a Gaudi Functional algorithm: the +`#include`s, the constructor with `KeyValue` / `KeyValues` wiring, the +`operator()` signature, a placeholder body, optional `Gaudi::Property` +members, and (optionally) a matching `CMakeLists.txt`. It supports both the +**k4FWCore** flavour used by Key4hep / FCC and the native +**Gaudi::Functional** flavour. + +The script is opinionated: it parses every CLI argument once, builds a +single `AlgorithmSpec`, and renders Jinja2 templates from it. There is no +in-place string surgery on the output, so the generated code is consistent +across the matrix of options. + +--- + +## Requirements + +- Python 3.9+ +- `Jinja2` (only needed for the plain-`python3` invocation path) + +The script ships a [PEP 723](https://peps.python.org/pep-0723/) inline +metadata block, so [`uv`](https://docs.astral.sh/uv/) can run it directly +without any manual environment setup. If you don't have `uv` installed, +`pipx install uv` or follow the install instructions on the uv site. + +## How to run it + +There are three equivalent ways to invoke the script: + +```bash +# 1. Recommended — uv resolves Python and Jinja2 from the PEP 723 block. +uv run generateFunctional MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' + +# 2. Direct execution via the shebang (requires uv on PATH). +chmod +x generateFunctional +./generateFunctional MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' + +# 3. Plain Python (you must have jinja2 installed in the active env). +python3 generateFunctional MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' +``` + +The first two routes are self-contained: nothing needs to be installed in +the system or active Python environment beyond `uv` itself. + +--- + +## Quick start + +```bash +# k4FWCore producer (functional type inferred from --outputs) +uv run generateFunctional MyProducer \ + -o 'edm4hep::MCParticleCollection:MCParticles' +``` + +That writes `MyProducer.cpp` in the current directory. Add `--cmake` to also +emit a `CMakeLists.txt`: + +```bash +uv run generateFunctional MyProducer \ + -o 'edm4hep::MCParticleCollection:MCParticles' \ + --cmake +``` + +--- + +## File-overwrite policy + +`generateFunctional` **never silently overwrites an existing file**. If the target +`.cpp` or `CMakeLists.txt` already exists, the script prints a diagnostic +and exits non-zero: + +``` +Refusing to overwrite existing CMakeLists.txt at 'CMakeLists.txt'. + Re-run with --force (or remove the file) if you really want to replace it. +``` + +Pass `--force` to allow overwriting. The check applies to both the source +file and the CMake file independently, so partial regeneration is fine +(e.g. delete just the `.cpp` and re-run without `--force`). + +--- + +## Arguments + +### Positional + +| Argument | Description | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `class_name` | Name of the C++ class to generate (e.g. `MyProducer`). | +| `functional_type` | Optional. One of `consumer`, `producer`, `transformer`, `filter`. If omitted, the type is inferred from the number of inputs and outputs. | + +### Inputs / outputs + +| Flag | Format | Notes | +| ------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `-i`, `--inputs` | `TYPE:KEY` (one or more) | If `:KEY` is omitted, a default key is derived from the type name (e.g. `edm4hep::MCParticleCollection` → `MCParticles`). | +| `-o`, `--outputs` | `TYPE:KEY` (one or more) | Multiple outputs trigger `MultiTransformer` and a `std::tuple` return type. Mutually exclusive with `--runtime-outputs`. | +| `--runtime-outputs` | `TYPE` | Dynamic output collections; `operator()` returns `std::vector`. **k4FWCore-only.** | + +### Vector / runtime inputs (k4FWCore) + +| Flag | Format | Notes | +| --------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `--runtime-inputs` | `TYPE:KEY:DEF1[,DEF2,...]` | Promotes the matching `--inputs` entry to a runtime `KeyValues` vector with the given default location names. | +| `--keyvalues-inputs` | `KEY[:LABEL]` | Per-input override: turn the named `--inputs` KEY(s) into vector inputs while leaving the others scalar. | +| `--all-keyvalues` | flag | Treat every `--inputs` entry as a `KeyValues` vector. | + +### Properties + +| Flag | Format | Notes | +| --------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `-p`, `--properties` | `type:name:default[:description]` | Emits `Gaudi::Property m_{this, "", , ""}` for each entry. | +| `--private-properties`| flag | Place `Gaudi::Property` members under a `private:` access label. | + +### Class shape & framework + +| Flag | Notes | +| ---------------- | -------------------------------------------------------------------------------------------------- | +| `-n`, `--namespace` | Wrap the generated class in a **C++ namespace** — `namespace { ... } // namespace ` around the class definition. Empty (default) leaves the class at global scope. Not to be confused with the Gaudi/k4FWCore framework namespace or the runtime algorithm instance name. | +| `--framework` | `k4fwcore` (default) or `gaudi` (vanilla `Gaudi::Functional`). | +| `--use-class` | Generate `class ... { public: ... }` instead of the default `struct`. | +| `--type-aliases` | Emit `using XxxColl = ...;` aliases for input collection types and use them in the operator signature. | +| `--event-context`| Add `const EventContext&` as the first `operator()` argument and scaffold a `finalize()` override. | + +### Output + +| Flag | Notes | +| ------------------- | -------------------------------------------------------------------------------------- | +| `-f`, `--output-file` | Path for the generated `.cpp`. Default: `.cpp` in the current directory. | +| `--cmake` | Also emit `CMakeLists.txt` next to the source. | +| `--force` | Allow overwriting existing files. Without this flag, the script refuses to clobber. | + +--- + +## Functional-type inference + +When the positional `functional_type` is omitted, the script picks one from +the I/O counts: + +| inputs | outputs | inferred type | +| -----: | ------: | ------------------ | +| > 0 | == 0 | `consumer` | +| == 0 | >= 1 | `producer` | +| >= 1 | == 1 | `transformer` | +| >= 1 | > 1 | `multitransformer` | + +`filter` is never inferred — supply it explicitly. + +If you write `transformer` but pass multiple `--outputs`, the script +auto-promotes to `multitransformer`. + +--- + +## Examples + +### Producer with multiple outputs and a property + +```bash +uv run generateFunctional MyProducer \ + -o 'edm4hep::MCParticleCollection:MCParticles' \ + 'edm4hep::TrackCollection:Tracks' \ + -p 'int:ExampleInt:3:An example integer property' +``` + +The output uses a `retType = std::tuple<...>` alias for readability. + +### Native Gaudi transformer wrapped in a C++ namespace + +```bash +uv run generateFunctional MySum \ + -i 'Input1:Loc1' 'Input2:Loc2' \ + -o 'Output:OutLoc' \ + --framework gaudi \ + -n MyNamespace +``` + +`-n MyNamespace` wraps the class definition in `namespace MyNamespace { ... }`. +The output also emits a +`BaseClass_t = Gaudi::Functional::Traits::BaseClass_t` +typedef and uses `Gaudi::Functional::Transformer<...>` as the base: + +```cpp +namespace MyNamespace { +struct MySum final : Gaudi::Functional::Transformer { + // ... +}; +} // namespace MyNamespace +``` + +### Variable-length / runtime inputs (k4FWCore) + +```bash +uv run generateFunctional MyVarConsumer \ + -i 'edm4hep::MCParticleCollection:Inputs' \ + --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' +``` + +`Inputs` is wired as `KeyValues("Inputs", {"MCParticles0", "MCParticles1"})` +and `operator()` receives `const std::vector&`. + +### Dynamic output collections + +```bash +uv run generateFunctional MyDynProducer \ + --runtime-outputs 'edm4hep::MCParticleCollection' +``` + +The constructor wires `KeyValues("OutputCollections", {"MCParticles"})` and +`operator()` returns `std::vector`. + +### Filter + +```bash +uv run generateFunctional MyFilter filter \ + -i 'edm4hep::MCParticleCollection:MCParticles' +``` + +Returns `bool`. + +--- + +## What ends up in the generated `.cpp` + +- A header banner with the exact command line used to generate the file. +- Framework header (`k4FWCore/.h` or `Gaudi/Functional/.h`). + For k4FWCore, `MultiTransformer` is included from `Transformer.h`. +- Auto-detected `edm4hep/.h` headers and `podio/UserDataCollection.h` + when applicable (podio headers are available transitively through `k4FWCore`). +- An optional `using BaseClass_t = ...;` for native Gaudi. +- Optional `using retType = std::tuple<...>;` (k4FWCore multi-output). +- Optional `using XxxColl = ...;` aliases (`--type-aliases`). +- The class itself: constructor, `initialize()` (when there are vector + inputs), `operator()`, properties, optional `finalize()` and bookkeeping + members (`--event-context`). +- `DECLARE_COMPONENT()` at the bottom. + +## What ends up in `CMakeLists.txt` + +- `find_package(k4FWCore REQUIRED)` or `find_package(Gaudi REQUIRED)`. +- `find_package(EDM4HEP REQUIRED)` if any collection type is from `edm4hep`. +- `find_package(podio REQUIRED)` if `podio::UserDataCollection` is used with + `--framework gaudi` (for k4fwcore, podio is a transitive dependency of + `k4FWCore::k4FWCore` and no explicit find is needed). +- `gaudi_add_module(Plugin SOURCES .cpp LINK ...)` + with the matching link libraries. + +--- + +## Exit codes + +| Code | Meaning | +| ---: | -------------------------------------------------------------------- | +| `0` | Generation succeeded. | +| `1` | A target file already existed and `--force` was not supplied. | +| `2` | argparse error (bad flag, mutually exclusive options, etc.). | + +## Common pitfalls + +- **`--runtime-outputs` + `--outputs`** — these are mutually exclusive. +- **`--runtime-outputs` + `--framework gaudi`** — k4FWCore-only. +- **Inferring `filter`** — the script will not infer this; pass `filter` + explicitly as the second positional argument. +- **Type keys** — keys like `MCParticles` are derived from the type name by + stripping `Collection` and adding `s`. Override with the explicit + `TYPE:KEY` form when that doesn't match your project's conventions. diff --git a/k4FWCore/helpers/generateFunctional b/k4FWCore/helpers/generateFunctional new file mode 100644 index 00000000..de01daeb --- /dev/null +++ b/k4FWCore/helpers/generateFunctional @@ -0,0 +1,987 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "jinja2>=3.0", +# ] +# /// +""" +Gaudi Functional C++ Class Generator +Generates boilerplate for Gaudi Functional algorithms in both the +k4FWCore and native Gaudi::Functional frameworks. + +Run with either: + uv run generateFunctional [args...] # uv resolves deps from the PEP 723 block + ./generateFunctional [args...] # uses the shebang (requires uv on PATH) + python3 generateFunctional [args...] # plain Python; needs jinja2 installed +""" +import argparse +import os +import re +import shlex +import sys +import textwrap +from dataclasses import dataclass +from typing import List, Optional + +from jinja2 import Environment, StrictUndefined + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- +_FRAMEWORK_NS = { + "k4fwcore": "k4FWCore", + "gaudi": "Gaudi::Functional", +} +_BASE_CLASS = { + "consumer": "Consumer", + "producer": "Producer", + "transformer": "Transformer", + "multitransformer": "MultiTransformer", + "filter": "FilterPredicate", +} + + +# --------------------------------------------------------------------------- +# Data classes (parsed once at the CLI boundary, never re-parsed) +# --------------------------------------------------------------------------- +@dataclass +class DataSpec: + """One input or output collection: a C++ type and a collection-location key.""" + type_name: str + key: str # collection-location name used in KeyValue / KeyValues + is_vector: bool = False # True for std::vector variable-length inputs + + @staticmethod + def _split_at_separator(spec: str) -> tuple: + """ + Split 'TypeName:LocationKey' at the *last* bare colon (not inside + angle-brackets, not part of a C++ '::' token). + Returns (type_str, key_str); key_str is '' when no separator is found. + """ + depth, last_sep = 0, -1 + for i, ch in enumerate(spec): + if ch == "<": + depth += 1 + elif ch == ">": + depth -= 1 + elif ch == ":" and depth == 0: + if not (i > 0 and spec[i - 1] == ":") and \ + not (i + 1 < len(spec) and spec[i + 1] == ":"): + last_sep = i + return (spec, "") if last_sep == -1 else (spec[:last_sep], spec[last_sep + 1:]) + + @staticmethod + def _default_key(type_name: str) -> str: + """edm4hep::MCParticleCollection -> MCParticles""" + base = type_name.split("::")[-1] + base = re.sub(r"<.*>", "", base) # strip template params + base = re.sub(r"Collection$", "", base) + return base + "s" + + @classmethod + def parse(cls, spec: str, is_vector: bool = False) -> "DataSpec": + type_name, key = cls._split_at_separator(spec) + if not key: + key = cls._default_key(type_name) + return cls(type_name=type_name, key=key, is_vector=is_vector) + + # Derived properties used in templates ----------------------------------- + @property + def edm4hep_header(self) -> Optional[str]: + """Return the edm4hep header filename for this type, or None. + + Bug fix: keep 'Collection' in the filename. + edm4hep::MCParticleCollection -> edm4hep/MCParticleCollection.h + """ + m = re.search(r"edm4hep::(\w+Collection)", self.type_name) + if m: + return m.group(1) + ".h" + return None + + @property + def needs_podio_header(self) -> bool: + return "podio::UserDataCollection" in self.type_name + + @property + def cpp_sig_type(self) -> str: + """C++ type as it appears in the template signature.""" + if self.is_vector: + return f"const std::vector&" + return f"const {self.type_name}&" + + +@dataclass +class RuntimeInputSpec: + """An input declared with KeyValues and received as std::vector&.""" + data: DataSpec + defaults: List[str] # initial default location names + + @classmethod + def parse(cls, spec: str) -> "RuntimeInputSpec": + """ + Format: TYPE:KEY:Default0[,Default1,...] + The KEY must match the key of a regular --inputs entry. + """ + depth, seps = 0, [] + for i, ch in enumerate(spec): + if ch == "<": + depth += 1 + elif ch == ">": + depth -= 1 + elif ch == ":" and depth == 0: + if not (i > 0 and spec[i - 1] == ":") and \ + not (i + 1 < len(spec) and spec[i + 1] == ":"): + seps.append(i) + if not seps: + key = DataSpec._default_key(spec) + return cls(data=DataSpec(spec, key, is_vector=True), defaults=[key]) + if len(seps) == 1: + p = seps[0] + type_name, key = spec[:p], spec[p + 1:] + return cls(data=DataSpec(type_name, key, is_vector=True), defaults=[key]) + p0, p1 = seps[0], seps[1] + type_name = spec[:p0] + key = spec[p0 + 1:p1] + defaults = [d.strip() for d in spec[p1 + 1:].split(",")] + return cls(data=DataSpec(type_name, key, is_vector=True), defaults=defaults) + + +@dataclass +class PropertySpec: + """A Gaudi::Property member declaration.""" + type_name: str + name: str + default: str + description: str + + @classmethod + def parse(cls, spec: str) -> "PropertySpec": + """Format: type:name:default[:description]""" + parts = spec.split(":", 3) + return cls( + type_name = parts[0], + name = parts[1] if len(parts) > 1 else parts[0], + default = parts[2] if len(parts) > 2 else "0", + description = parts[3] if len(parts) > 3 else "", + ) + + @property + def member_name(self) -> str: + """Return the C++ member variable name. + + Bug fix: lowercase the first character of the name after the 'm_' prefix + so that e.g. 'Offset' -> 'm_offset', not 'm_Offset'. + """ + n = self.name + if n.startswith("m_"): + return n + return f"m_{n[0].lower()}{n[1:]}" + + +@dataclass +class AlgorithmSpec: + """ + Fully parsed, validated description of the algorithm to generate. + This is the single object threaded through all generator methods. + """ + class_name: str + functional_type: str # consumer | producer | transformer | multitransformer | filter + inputs: List[DataSpec] + outputs: List[DataSpec] + runtime_output: Optional[DataSpec] # set when --runtime-outputs is used + runtime_defaults: dict # key -> [default, ...] from --runtime-inputs + properties: List[PropertySpec] + namespace: Optional[str] + framework: str # k4fwcore | gaudi + use_class: bool + type_aliases: bool + private_props: bool + all_keyvalues: bool + event_context: bool + generate_cmake: bool + output_file: str + command_line: str + + # --- Derived helpers (used by templates) -------------------------------- + @property + def is_k4(self) -> bool: + return self.framework == "k4fwcore" + + @property + def is_runtime(self) -> bool: + return self.runtime_output is not None + + @property + def framework_ns(self) -> str: + return _FRAMEWORK_NS[self.framework] + + @property + def base_short(self) -> str: + return _BASE_CLASS[self.functional_type] + + @property + def base_full(self) -> str: + return f"{self.framework_ns}::{self.base_short}" + + @property + def use_ret_type_alias(self) -> bool: + return self.is_k4 and not self.is_runtime and len(self.outputs) > 1 + + @property + def cpp_return_type(self) -> str: + if self.functional_type == "consumer": + return "void" + if self.functional_type == "filter": + return "bool" + if self.is_runtime: + return f"std::vector<{self.runtime_output.type_name}>" + if self.use_ret_type_alias: + return "retType" + if len(self.outputs) == 1: + return self.outputs[0].type_name + return "std::tuple<{}>".format(", ".join(o.type_name for o in self.outputs)) + + @property + def template_signature(self) -> str: + """ReturnType(const In1&, const In2&, ...)""" + in_parts = [inp.cpp_sig_type for inp in self.inputs] + if self.event_context: + in_parts = ["const EventContext&"] + in_parts + sig = "{}({})".format(self.cpp_return_type, ", ".join(in_parts)) + if not self.is_k4: + sig += ", BaseClass_t" + return sig + + @property + def type_alias_pairs(self) -> List[tuple]: + """[(alias_name, full_type), ...] for --type-aliases mode.""" + if not self.type_aliases: + return [] + seen: dict = {} + result = [] + used_aliases: set = set() + for inp in self.inputs: + t = inp.type_name + if t in seen: + continue + inner = re.search(r"<([^>]+)>", t) + if inner: + base = inner.group(1).strip().split("::")[-1].capitalize() + else: + stem = re.sub(r"Collection$", "", t.split("::")[-1]) + if stem.endswith("Link"): + base = "Link" + elif "Hit" in stem: + base = re.sub(r"\d+[A-Z]?$", "", stem) or stem + elif stem.startswith("Reconstructed"): + base = "Reco" + else: + words = re.findall(r"[A-Z][a-z0-9]*", stem) + base = words[-1] if words else stem + alias = base + "Coll" + suffix, candidate = 2, alias + while candidate in used_aliases: + candidate = f"{alias}{suffix}" + suffix += 1 + used_aliases.add(candidate) + seen[t] = candidate + result.append((candidate, t)) + return result + + def display_type(self, data: DataSpec) -> str: + """Return alias name for a type if --type-aliases is active, else full type.""" + if not self.type_aliases: + return data.type_name + lookup = {t: a for a, t in self.type_alias_pairs} + return lookup.get(data.type_name, data.type_name) + + +# --------------------------------------------------------------------------- +# Parsing helpers +# --------------------------------------------------------------------------- +def _infer_functional_type( + inputs: List[DataSpec], + outputs: List[DataSpec], + runtime_output: Optional[DataSpec], + explicit: Optional[str], +) -> str: + """ + Determine the functional type. When the caller supplies an explicit value + it is honoured (after auto-promoting transformer -> multitransformer). + Otherwise the type is inferred from the number of inputs and outputs. + """ + n_in = len(inputs) + n_out = len(outputs) + (1 if runtime_output else 0) + + if explicit: + ft = explicit.lower() + # Auto-promote: user wrote 'transformer' but gave multiple outputs + if ft == "transformer" and n_out > 1: + ft = "multitransformer" + return ft + + # Inference table + if n_in > 0 and n_out == 0: + return "consumer" + if n_in == 0 and n_out > 0: + return "producer" + if n_in > 0 and n_out == 1: + return "transformer" + if n_in > 0 and n_out > 1: + return "multitransformer" + raise ValueError( + "Cannot infer functional type: supply at least one --inputs or --outputs." + ) + + +def _build_spec(args: argparse.Namespace) -> AlgorithmSpec: + """ + Convert the raw argparse namespace into a fully validated AlgorithmSpec. + All string parsing happens here and nowhere else. + """ + # --- runtime-inputs: parse first so we know which keys are vector ------- + runtime_input_specs: List[RuntimeInputSpec] = [ + RuntimeInputSpec.parse(s) for s in (args.runtime_inputs or []) + ] + runtime_input_keys = {rs.data.key for rs in runtime_input_specs} + runtime_defaults = {rs.data.key: rs.defaults for rs in runtime_input_specs} + + # --- keyvalues-inputs overrides ----------------------------------------- + kvi_map: dict = {} + for s in (args.keyvalues_inputs or []): + parts = s.split(":", 1) + kvi_map[parts[0]] = parts[1] if len(parts) > 1 else parts[0] + + # --- inputs ------------------------------------------------------------- + inputs: List[DataSpec] = [] + for raw in (args.inputs or []): + ds = DataSpec.parse(raw) + if ds.key in runtime_input_keys: + # Promote to the RuntimeInputSpec's DataSpec (is_vector=True) + rs = next(r for r in runtime_input_specs if r.data.key == ds.key) + inputs.append(rs.data) + elif ds.key in kvi_map or getattr(args, "all_keyvalues", False): + inputs.append(DataSpec(ds.type_name, ds.key, is_vector=True)) + else: + inputs.append(ds) + + # --- outputs ------------------------------------------------------------ + outputs: List[DataSpec] = [DataSpec.parse(raw) for raw in (args.outputs or [])] + + # --- runtime output (dynamic vector return) ----------------------------- + runtime_output: Optional[DataSpec] = ( + DataSpec.parse(args.runtime_outputs) if args.runtime_outputs else None + ) + + # --- properties --------------------------------------------------------- + properties = [PropertySpec.parse(p) for p in (args.properties or [])] + + # --- functional type (inferred or explicit) ----------------------------- + functional_type = _infer_functional_type( + inputs, outputs, runtime_output, + explicit=getattr(args, "functional_type", None), + ) + + return AlgorithmSpec( + class_name = args.class_name, + functional_type = functional_type, + inputs = inputs, + outputs = outputs, + runtime_output = runtime_output, + runtime_defaults= runtime_defaults, + properties = properties, + namespace = args.namespace or None, + framework = args.framework, + use_class = args.use_class, + type_aliases = getattr(args, "type_aliases", False), + private_props = getattr(args, "private_properties", False), + all_keyvalues = getattr(args, "all_keyvalues", False), + event_context = getattr(args, "event_context", False), + generate_cmake = getattr(args, "cmake", False), + output_file = args.output_file or f"{args.class_name}.cpp", + command_line = " ".join(shlex.quote(a) for a in sys.argv), + ) + + +# --------------------------------------------------------------------------- +# Jinja2 templates +# --------------------------------------------------------------------------- +_CPP_TEMPLATE = """\ +// Generated by Gaudi Functional C++ Class Generator +// Command: {{ spec.command_line }} +{{ includes }} + +{% if not spec.is_k4 %} +using BaseClass_t = Gaudi::Functional::Traits::BaseClass_t; +{% endif %} +{% if spec.use_ret_type_alias %} +// Which type of collections we are producing +using retType = std::tuple< +{% for out in spec.outputs %} + {{ out.type_name }}{{ "" if loop.last else "," }} +{% endfor %} +>; +{% endif %} +{% if spec.type_alias_pairs %} +// Which type of collections we are reading +{% for alias, typ in spec.type_alias_pairs %} +using {{ alias }} = {{ typ }}; +{% endfor %} +{% endif %} + +{% if spec.namespace %} +namespace {{ spec.namespace }} { +{% endif %} +{{ class_kw }} {{ cls }} final : {{ spec.base_full }}<{{ spec.template_signature }}> { +{% if access_kw %}{{ access_kw }}{% endif %} + // Constructor: KeyValues map to collection names, settable from Python +{{ constructor }} + +{% if spec.inputs | selectattr('is_vector') | list %} + StatusCode initialize() override { + // Verify input locations are set from Python before the event loop +{% for inp in spec.inputs %} +{% if inp.is_vector %} + // inputLocations("{{ inp.key }}") -> current list of collection names +{% endif %} +{% endfor %} + return StatusCode::SUCCESS; + } + +{% endif %} + // This is the function that will be called to produce the data + {{ op_signature }} { +{{ op_body }} + } + +{% if spec.event_context %} + StatusCode finalize() override { + // TODO: finalise event-context state + return StatusCode::SUCCESS; + } + +{% endif %} +{% if spec.properties or spec.event_context %} +{% if spec.private_props or spec.event_context %} +private: +{% endif %} +{% for prop in spec.properties %} + Gaudi::Property<{{ prop.type_name }}> {{ prop.member_name }}{ + this, "{{ prop.name }}", {{ prop.default }}{{ ', "' + prop.description + '"' if prop.description else '' }}}; +{% endfor %} +{% if spec.event_context %} + mutable std::set m_eventNumbersSeen{}; + mutable std::mutex m_mutex{}; +{% endif %} +{% endif %} +}; +{% if spec.namespace %} +} // namespace {{ spec.namespace }} +{% endif %} + +DECLARE_COMPONENT({% if spec.namespace %}{{ spec.namespace }}::{% endif %}{{ cls }}) +""" + +_CMAKE_TEMPLATE = """\ +# Generated by Gaudi Functional C++ Class Generator +# Command: {{ spec.command_line }} +cmake_minimum_required(VERSION 3.15) +project({{ spec.class_name }} LANGUAGES CXX) + +{% for pkg in find_packages %}{{ pkg }} +{% endfor %} + +gaudi_add_module({{ spec.class_name }}Plugin + SOURCES {{ spec.class_name }}.cpp + LINK +{% for lib in link_libs %} {{ lib }} +{% endfor %}) +""" + + +# --------------------------------------------------------------------------- +# Code generation (pure Python logic, no string surgery) +# --------------------------------------------------------------------------- +def _build_includes(spec: AlgorithmSpec) -> str: + lines = [] + # Framework header — MultiTransformer lives in Transformer.h for k4FWCore + header_base = "Transformer" if spec.functional_type == "multitransformer" and spec.is_k4 \ + else spec.base_short + if spec.is_k4: + lines.append(f'#include "k4FWCore/{header_base}.h"') + else: + lines.append(f'#include "Gaudi/Functional/{header_base}.h"') + if spec.properties: + lines.append('#include "Gaudi/Property.h"') + + edm_headers: set = set() + podio_needed = False + all_ds = spec.inputs + spec.outputs + ([spec.runtime_output] if spec.runtime_output else []) + for ds in all_ds: + if ds.edm4hep_header: + edm_headers.add(ds.edm4hep_header) + if ds.needs_podio_header: + podio_needed = True + for h in sorted(edm_headers): + lines.append(f'#include "edm4hep/{h}"') + if podio_needed: + lines.append('#include "podio/UserDataCollection.h"') + lines.append("#include ") + if spec.event_context: + lines += ["#include ", "#include ", "#include "] + if len(spec.outputs) > 1 and not spec.use_ret_type_alias: + lines.append("#include ") + if spec.is_runtime or any(inp.is_vector for inp in spec.inputs): + lines.append("#include ") + return "\n".join(lines) + + +def _build_constructor(spec: AlgorithmSpec) -> str: + """Return the full constructor definition (k4FWCore style).""" + cls = spec.class_name + base = spec.base_short + rd = spec.runtime_defaults + ft = spec.functional_type + + def _kv(ds: DataSpec) -> str: + if ds.is_vector: + defs_str = ", ".join(f'"{d}"' for d in rd.get(ds.key, [ds.key])) + return f'KeyValues("{ds.key}", {{{defs_str}}})' + return f'KeyValue("{ds.key}", "{ds.key}")' + + def _brace_block(items: list, indent: int = 20, base_indent: int = 16) -> str: + """Build a brace-wrapped list of KeyValues, always including the braces. + + Bug fix: single-item blocks are now emitted as '{KeyValue(...)}' rather + than bare 'KeyValue(...)'. Consumer/filter pass through _bare_block + instead and are unaffected. + """ + if not items: + return "{}" + if len(items) == 1: + return "{" + _kv(items[0]) + "}" + ind = " " * indent + body = (",\n" + ind).join(_kv(it) for it in items) + return "{\n" + ind + body + ",\n" + " " * base_indent + "}" + + def _bare_block(items: list) -> str: + """For consumer/filter: single collection bare, multiple in braces.""" + if not items: + return "{}" + if len(items) == 1: + return _kv(items[0]) + ind = " " * 20 + body = (",\n" + ind).join(_kv(it) for it in items) + return "{\n" + ind + body + ",\n" + " " * 16 + "}" + + # consumer / filter: inputs are passed bare (no outer braces for single) + if ft in ("consumer", "filter"): + in_block = _bare_block(spec.inputs) + return ( + f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" + f" : {base}(name, svcLoc, {in_block}) {{}}" + ) + + # producer / transformer / multitransformer: always use brace-wrapped blocks + in_block = _brace_block(spec.inputs) + + if spec.is_runtime: + out_key = spec.runtime_output.key + out_block = f'{{KeyValues("OutputCollections", {{"{out_key}"}})}}' + return ( + f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" + f" : {base}(name, svcLoc, {in_block}, {out_block}) {{}}" + ) + if not spec.outputs: + return ( + f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" + f" : {base}(name, svcLoc, {in_block}, {{}}) {{}}" + ) + + out_block = _brace_block(spec.outputs, indent=17, base_indent=17) + + if len(spec.outputs) == 1: + return ( + f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" + f" : {base}(name, svcLoc, {in_block}, {out_block}) {{}}" + ) + # Multiple fixed outputs + return ( + f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" + f" : {base}(name, svcLoc, {in_block},\n" + f" {out_block}) {{}}" + ) + + +def _build_constructor_gaudi(spec: AlgorithmSpec) -> str: + cls = spec.class_name + base = spec.base_short + + def _kv(ds: DataSpec) -> str: + return f'KeyValue{{"{ds.key}", "{ds.key}"}}' + + in_kvs = [_kv(ds) for ds in spec.inputs] + out_kvs = [_kv(ds) for ds in spec.outputs] + + # Native Gaudi Transformer constructor variants (details.h): + # (name, svc, KeyValue_in, KeyValue_out) — 1 in, 1 out + # (name, svc, KeyValue_in, RepeatValues__out) — 1 in, N out + # (name, svc, RepeatValues__in, KeyValue_out) — N in, 1 out + # (name, svc, RepeatValues__in, RepeatValues__out) — N in, M out + # RepeatValues_ is a tuple of pairs, passed as a braced list {kv1, kv2, ...}. + # Single KeyValues must NOT be wrapped in braces. + def _arg(kvs: list[str]) -> str: + if len(kvs) == 0: + return "" + if len(kvs) == 1: + return kvs[0] + return "{" + ", ".join(kvs) + "}" + + parts = [p for p in [_arg(in_kvs), _arg(out_kvs)] if p] + sep = ", " if parts else "" + args_str = ", ".join(parts) + return ( + f" {cls}(const std::string& name, ISvcLocator* svcLoc)\n" + f" : {base}(name, svcLoc{sep}{args_str}) {{}}" + ) + + +def _build_op_signature(spec: AlgorithmSpec) -> str: + params = [] + if spec.event_context: + params.append("const EventContext& ctx") + for inp in spec.inputs: + disp = spec.display_type(inp) + if inp.is_vector: + params.append(f"const std::vector& {inp.key}") + else: + params.append(f"const {disp}& {inp.key}") + return f"{spec.cpp_return_type} operator()({', '.join(params)}) const override" + + +def _build_op_body(spec: AlgorithmSpec) -> str: + ft = spec.functional_type + if spec.is_runtime: + elem = spec.runtime_output.type_name + return ( + f" const auto locs = outputLocations();\n" + f" std::vector<{elem}> outputCollections;\n" + f" for (size_t i = 0; i < locs.size(); ++i) {{\n" + f" auto coll = {elem}();\n" + f" // TODO: fill coll\n" + f" outputCollections.emplace_back(std::move(coll));\n" + f" }}\n" + f" return outputCollections;" + ) + if ft == "consumer": + lines = [] + if spec.event_context: + lines.append(' info() << "Event number is " << ctx.evt() << endmsg;') + for inp in spec.inputs: + lines += [ + f' debug() << "Received {inp.key} with " << {inp.key}.size() << " elements" << endmsg;', + f" for (const auto& elem : {inp.key}) {{", + f" // TODO: process elem", + f" }}", + ] + return "\n".join(lines) if lines else " // TODO: implement" + if ft == "filter": + return " // TODO: implement filter logic\n return false;" + if len(spec.outputs) == 1: + return ( + f" // TODO: implement\n" + f" return {spec.outputs[0].type_name}{{}};" + ) + # Multiple outputs + lines = [] + for i, out in enumerate(spec.outputs, 1): + lines.append(f" auto output{i} = {out.type_name}();") + lines += [ + "", + " // TODO: fill output collections", + "", + ] + moves = ", ".join(f"std::move(output{i})" for i in range(1, len(spec.outputs) + 1)) + lines.append(f" return std::make_tuple({moves}); // NOLINT") + return "\n".join(lines) + + +def _build_cmake_context(spec: AlgorithmSpec) -> dict: + all_ds = spec.inputs + spec.outputs + ([spec.runtime_output] if spec.runtime_output else []) + has_edm4hep = any(ds.edm4hep_header for ds in all_ds) + has_podio = any(ds.needs_podio_header for ds in all_ds) + + find_packages, link_libs = [], [] + if spec.is_k4: + find_packages.append("find_package(k4FWCore REQUIRED)") + link_libs.append("k4FWCore::k4FWCore") + else: + find_packages.append("find_package(Gaudi REQUIRED)") + link_libs += ["Gaudi::GaudiKernel"] + if has_edm4hep: + find_packages.append("find_package(EDM4HEP REQUIRED)") + link_libs.append("EDM4HEP::edm4hep") + if has_podio and not spec.is_k4: + # For k4fwcore, podio is a transitive dependency of k4FWCore::k4FWCore + # (declared in k4FWCoreConfig.cmake.in), so no explicit find/link needed. + find_packages.append("find_package(podio REQUIRED)") + link_libs.append("podio::podio") + return {"find_packages": find_packages, "link_libs": link_libs} + + +# --------------------------------------------------------------------------- +# Top-level generate() +# --------------------------------------------------------------------------- +def generate(spec: AlgorithmSpec) -> tuple: + """ + Render the C++ source (and optionally CMakeLists.txt) for *spec*. + Returns (cpp_source, cmake_source_or_None). + """ + env = Environment( + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True, + undefined=StrictUndefined, + ) + constructor = ( + _build_constructor(spec) if spec.is_k4 + else _build_constructor_gaudi(spec) + ) + cpp_ctx = { + "spec": spec, + "cls": spec.class_name, + "includes": _build_includes(spec), + "constructor": constructor, + "op_signature": _build_op_signature(spec), + "op_body": _build_op_body(spec), + "class_kw": "struct" if not spec.use_class else "class", + "access_kw": "public:\n" if spec.use_class else "", + } + cpp_source = env.from_string(_CPP_TEMPLATE).render(**cpp_ctx).lstrip("\n") + + cmake_source = None + if spec.generate_cmake: + cmake_ctx = {"spec": spec, **_build_cmake_context(spec)} + cmake_source = env.from_string(_CMAKE_TEMPLATE).render(**cmake_ctx) + + return cpp_source, cmake_source + + +# --------------------------------------------------------------------------- +# Safe-write helper +# --------------------------------------------------------------------------- +def _safe_write(path: str, content: str, force: bool, label: str) -> bool: + """ + Write *content* to *path*. If the file exists and *force* is False, refuse + to overwrite and tell the user how to override. Returns True on success. + """ + if os.path.exists(path) and not force: + print( + f"Refusing to overwrite existing {label} at {path!r}.\n" + f" Re-run with --force (or remove the file) if you really want to replace it.", + file=sys.stderr, + ) + return False + with open(path, "w") as fh: + fh.write(content) + print(f"Written to {path}", file=sys.stderr) + return True + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="generateFunctional", + description="Generate Gaudi Functional C++ algorithm boilerplate.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=textwrap.dedent("""\ + Functional type is inferred from the number of inputs and outputs: + consumer inputs > 0, outputs == 0 + producer inputs == 0, outputs >= 1 + transformer inputs >= 1, outputs == 1 + multitransformer inputs >= 1, outputs > 1 + filter inputs >= 1, supply --type filter explicitly + + Examples: + # k4FWCore producer (type inferred) + generateFunctional MyProducer -o 'edm4hep::MCParticleCollection:MCParticles' + + # k4FWCore multi-output producer with properties + generateFunctional MyProducer \\ + -o 'edm4hep::MCParticleCollection:MCParticles' \\ + 'edm4hep::TrackCollection:Tracks' \\ + -p 'int:ExampleInt:3:An example integer property' + + # Gaudi transformer wrapped in 'namespace MyNamespace { ... }' (type inferred) + generateFunctional MySum -i 'Input1:Loc1' 'Input2:Loc2' -o 'Output:OutLoc' \\ + --framework gaudi -n MyNamespace + + # Variable-length inputs (k4FWCore only) + generateFunctional MyVarConsumer \\ + -i 'edm4hep::MCParticleCollection:Inputs' \\ + --runtime-inputs 'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1' + """), + ) + parser.add_argument( + "class_name", + help="Name of the C++ class to generate (e.g. MyProducer).", + ) + parser.add_argument( + "functional_type", nargs="?", + choices=["consumer", "producer", "transformer", "filter"], + help=( + "Functional type. Omit to infer from --inputs / --outputs counts; " + "supply 'filter' explicitly when a FilterPredicate is wanted." + ), + ) + parser.add_argument( + "-i", "--inputs", nargs="*", default=[], metavar="TYPE:KEY", + help=( + "Input collections, one or more 'TYPE:KEY' specs. " + "Example: 'edm4hep::MCParticleCollection:MCParticles'. " + "If KEY is omitted, a default is derived from the type name." + ), + ) + # --outputs and --runtime-outputs are alternative ways to declare outputs; + # let argparse enforce that at parse time. + out_group = parser.add_mutually_exclusive_group() + out_group.add_argument( + "-o", "--outputs", nargs="*", default=[], metavar="TYPE:KEY", + help=( + "Output collections, one or more 'TYPE:KEY' specs. " + "Multiple outputs trigger MultiTransformer/tuple return generation. " + "Mutually exclusive with --runtime-outputs." + ), + ) + out_group.add_argument( + "--runtime-outputs", dest="runtime_outputs", default=None, metavar="TYPE", + help=( + "Enable dynamic output collections returning std::vector. " + "k4FWCore-only. Mutually exclusive with --outputs." + ), + ) + parser.add_argument( + "-p", "--properties", nargs="*", default=[], + metavar="TYPE:NAME:DEFAULT[:DESC]", + help=( + "Gaudi::Property members to declare. Format " + "'type:name:default[:description]'. Example " + "'int:NumThreads:4:Worker thread count'." + ), + ) + parser.add_argument( + "-n", "--namespace", default="", metavar="NAME", + help=( + "Wrap the generated class in a C++ namespace, i.e. emit " + "'namespace NAME { ... } // namespace NAME' around the class " + "definition. Empty (default) leaves the class at global scope. " + "Note: this is the C++ namespace, not the Gaudi/k4FWCore framework " + "namespace and not the runtime algorithm instance name." + ), + ) + parser.add_argument( + "--framework", choices=["gaudi", "k4fwcore"], default="k4fwcore", + help=( + "Target framework: 'k4fwcore' (default, for Key4hep / FCC) or " + "'gaudi' (vanilla Gaudi::Functional)." + ), + ) + parser.add_argument( + "--use-class", dest="use_class", action="store_true", default=False, + help="Generate 'class ... { public: ... }' instead of the default 'struct'.", + ) + parser.add_argument( + "-f", "--output-file", dest="output_file", default=None, + help="Path for the generated .cpp file. Default: .cpp in the cwd.", + ) + parser.add_argument( + "--type-aliases", dest="type_aliases", action="store_true", default=False, + help=( + "Emit 'using XxxColl = ...;' aliases for input collection types " + "and use them in the operator() signature." + ), + ) + parser.add_argument( + "--private-properties", dest="private_properties", + action="store_true", default=False, + help="Place Gaudi::Property members under a 'private:' access label.", + ) + parser.add_argument( + "--all-keyvalues", dest="all_keyvalues", + action="store_true", default=False, + help=( + "Treat every --inputs entry as a runtime KeyValues vector " + "(std::vector&) instead of a single KeyValue." + ), + ) + parser.add_argument( + "--keyvalues-inputs", dest="keyvalues_inputs", nargs="*", default=None, + metavar="KEY[:LABEL]", + help=( + "Per-input override: turn the named --inputs KEY(s) into KeyValues " + "vector inputs while leaving the others as scalars." + ), + ) + parser.add_argument( + "--runtime-inputs", dest="runtime_inputs", nargs="*", default=None, + metavar="TYPE:KEY:DEF1[,DEF2,...]", + help=( + "Declare runtime (variable-length) inputs with default location " + "names. Example " + "'edm4hep::MCParticleCollection:Inputs:MCParticles0,MCParticles1'. " + "k4FWCore-only." + ), + ) + parser.add_argument( + "--event-context", dest="event_context", + action="store_true", default=False, + help=( + "Add 'const EventContext&' as the first operator() argument and " + "scaffold a finalize() override with mutable bookkeeping members." + ), + ) + parser.add_argument( + "--cmake", action="store_true", default=False, + help="Also emit a CMakeLists.txt next to the source (cwd).", + ) + parser.add_argument( + "--force", action="store_true", default=False, + help=( + "Allow overwriting existing files. Without this flag, the script " + "refuses to clobber an existing .cpp or CMakeLists.txt." + ), + ) + return parser + + +def main() -> None: + parser = _build_parser() + args = parser.parse_args() + + # --outputs vs --runtime-outputs is enforced by the mutually-exclusive + # group on the parser; only the framework constraint remains here. + if args.runtime_outputs and args.framework != "k4fwcore": + parser.error("--runtime-outputs is only supported with --framework k4fwcore.") + + try: + spec = _build_spec(args) + except ValueError as exc: + # parser.error() prints usage and calls sys.exit(2); never returns. + parser.error(str(exc)) + + cpp_source, cmake_source = generate(spec) + + cpp_ok = _safe_write(spec.output_file, cpp_source, args.force, label="C++ source") + cmake_ok = True + if cmake_source is not None: + cmake_ok = _safe_write("CMakeLists.txt", cmake_source, args.force, label="CMakeLists.txt") + + if not (cpp_ok and cmake_ok): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/k4FWCore/helpers/tests/CMakeLists.txt b/k4FWCore/helpers/tests/CMakeLists.txt new file mode 100644 index 00000000..b6345f5f --- /dev/null +++ b/k4FWCore/helpers/tests/CMakeLists.txt @@ -0,0 +1,42 @@ +#[[ +Copyright (c) 2014-2024 Key4hep-Project. + +This file is part of Key4hep. +See https://key4hep.github.io/key4hep-doc/ for further info. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]] + +set(GENERATE_FUNCTIONAL "${PROJECT_SOURCE_DIR}/k4FWCore/helpers/generateFunctional") +set(GF_TESTS_DIR "${CMAKE_CURRENT_LIST_DIR}") + +foreach(test + producer + consumer + transformer + multitransformer + filter + runtime_consumer + runtime_transformer + event_context + gaudi_framework) + add_test(NAME GenerateFunctional_${test} + COMMAND bash "${GF_TESTS_DIR}/test_${test}.sh" + ) + # Forward the parent build's generator and C++ compiler so the inner + # cmake invocation in the test scripts doesn't fall back to "Unix Makefiles" + # (which fails in CI where only Ninja is available and make is not on PATH). + set_tests_properties(GenerateFunctional_${test} PROPERTIES + ENVIRONMENT "GENERATEFUNCTIONAL=${GENERATE_FUNCTIONAL};CMAKE_GENERATOR=${CMAKE_GENERATOR};CXX=${CMAKE_CXX_COMPILER}" + ) +endforeach() diff --git a/k4FWCore/helpers/tests/README.md b/k4FWCore/helpers/tests/README.md new file mode 100644 index 00000000..59905cf4 --- /dev/null +++ b/k4FWCore/helpers/tests/README.md @@ -0,0 +1,86 @@ + +# generateFunctional tests + +Each test generates a C++ algorithm with `generateFunctional`, configures it +with `cmake`, and compiles it. The tests are registered with CTest and run as +part of the standard k4FWCore test suite. + +--- + +## Running via CTest (standard) + +```bash +# Source the Key4hep environment +source /cvmfs/sw.hsf.org/key4hep/setup.sh + +# Build (tests are enabled by default) +mkdir build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +make -j$(nproc) + +# Run only the generateFunctional tests +ctest -R GenerateFunctional --output-on-failure +``` + +Each test takes ~1–3 minutes on lxplus (cmake configure + compile per test). +Run them in parallel with `ctest -j9 -R GenerateFunctional`. + +--- + +## Running a single test manually + +The bash scripts can also be run directly without building or installing, +as long as the Key4hep environment is sourced: + +```bash +source /cvmfs/sw.hsf.org/key4hep/setup.sh +bash k4FWCore/helpers/tests/test_producer.sh +``` + +The script finds `generateFunctional` via (in order): +1. `GENERATEFUNCTIONAL` env var (set automatically by CTest) +2. Installed `generateFunctional` on `PATH` +3. `../generateFunctional` relative to the `tests/` directory + +--- + +## What each test covers + +| Script | CTest name | Feature | +|---|---|---| +| `test_producer.sh` | `GenerateFunctional_producer` | Single output, property | +| `test_consumer.sh` | `GenerateFunctional_consumer` | Single input, property | +| `test_transformer.sh` | `GenerateFunctional_transformer` | Single in/out, `--private-properties` | +| `test_multitransformer.sh` | `GenerateFunctional_multitransformer` | Multiple in/out, `--type-aliases`, `podio::UserDataCollection` | +| `test_filter.sh` | `GenerateFunctional_filter` | `FilterPredicate` | +| `test_runtime_consumer.sh` | `GenerateFunctional_runtime_consumer` | `--runtime-inputs` / `KeyValues` vector input | +| `test_runtime_transformer.sh` | `GenerateFunctional_runtime_transformer` | `--runtime-outputs` / `std::vector` return | +| `test_event_context.sh` | `GenerateFunctional_event_context` | `--event-context`, `finalize()` placement | +| `test_gaudi_framework.sh` | `GenerateFunctional_gaudi_framework` | `--framework gaudi`, `--namespace` | + +--- + +## Notes + +- Each test creates an isolated `mktemp -d` sandbox, cleaned up automatically on exit. +- `_test_common.sh` is sourced by all test scripts — do not run it directly. +- Tests require the Key4hep environment on `CMAKE_PREFIX_PATH`. Source + `setup.sh` before building or running tests manually. +- `uv` is not required — the tests invoke `python3` directly. diff --git a/k4FWCore/helpers/tests/_test_common.sh b/k4FWCore/helpers/tests/_test_common.sh new file mode 100644 index 00000000..bd32c0f3 --- /dev/null +++ b/k4FWCore/helpers/tests/_test_common.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +# _test_common.sh — sourced by every test_*.sh script. +# Provides: GENERATOR path, SANDBOX temp dir, and run_cmake_build(). +# +# Usage in a test script: +# source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" +# run_cmake_build ClassName [generateFunctional args...] + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Resolve the generator: +# 1. GENERATEFUNCTIONAL env var (set by CTest via CMakeLists.txt) +# 2. Installed command on PATH +# 3. Source copy one level above this directory +if [[ -n "${GENERATEFUNCTIONAL:-}" ]]; then + GENERATOR="${GENERATEFUNCTIONAL}" +elif command -v generateFunctional &>/dev/null; then + GENERATOR="$(command -v generateFunctional)" +else + GENERATOR="${SCRIPT_DIR}/../generateFunctional" +fi + +if [[ ! -f "${GENERATOR}" ]]; then + echo "ERROR: generateFunctional not found (tried PATH and ${GENERATOR})" >&2 + exit 1 +fi + +SANDBOX="$(mktemp -d)" +trap 'rm -rf "${SANDBOX}"' EXIT + +# run_cmake_build [generateFunctional args...] +# 1. Generates .cpp + CMakeLists.txt via generateFunctional --cmake +# 2. Configures with cmake +# 3. Builds with cmake --build +run_cmake_build() { + local class="$1"; shift + ( + cd "${SANDBOX}" + python3 "${GENERATOR}" "${class}" "$@" --cmake --force + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build + ) +} diff --git a/k4FWCore/helpers/tests/test_consumer.sh b/k4FWCore/helpers/tests/test_consumer.sh new file mode 100644 index 00000000..5cf02596 --- /dev/null +++ b/k4FWCore/helpers/tests/test_consumer.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +# test_consumer.sh — build-test: k4FWCore Consumer (single input, one property) +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyConsumer \ + -i 'edm4hep::MCParticleCollection:InputCollection' \ + -p 'int:Offset:10:Integer to add to values' + +echo "PASS: consumer" diff --git a/k4FWCore/helpers/tests/test_event_context.sh b/k4FWCore/helpers/tests/test_event_context.sh new file mode 100644 index 00000000..d725bfc7 --- /dev/null +++ b/k4FWCore/helpers/tests/test_event_context.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +# test_event_context.sh — build-test: k4FWCore Transformer with EventContext and finalize() +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyEventContextTransformer \ + -i 'edm4hep::MCParticleCollection:InputCollection' \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + --event-context \ + --private-properties \ + -p 'int:Offset:10:Integer to add to values' + +echo "PASS: event_context" diff --git a/k4FWCore/helpers/tests/test_filter.sh b/k4FWCore/helpers/tests/test_filter.sh new file mode 100644 index 00000000..da181440 --- /dev/null +++ b/k4FWCore/helpers/tests/test_filter.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +# test_filter.sh — build-test: k4FWCore FilterPredicate +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyFilter filter \ + -i 'edm4hep::MCParticleCollection:InputCollection' + +echo "PASS: filter" diff --git a/k4FWCore/helpers/tests/test_gaudi_framework.sh b/k4FWCore/helpers/tests/test_gaudi_framework.sh new file mode 100644 index 00000000..586ea592 --- /dev/null +++ b/k4FWCore/helpers/tests/test_gaudi_framework.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +# test_gaudi_framework.sh — build-test: native Gaudi::Functional Transformer with namespace +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyGaudiTransformer \ + -i 'edm4hep::MCParticleCollection:InputCollection' \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + --framework gaudi \ + --namespace MyNamespace \ + -p 'int:Offset:10:Integer to add to values' + +echo "PASS: gaudi_framework" diff --git a/k4FWCore/helpers/tests/test_multitransformer.sh b/k4FWCore/helpers/tests/test_multitransformer.sh new file mode 100644 index 00000000..0990f9f4 --- /dev/null +++ b/k4FWCore/helpers/tests/test_multitransformer.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +# test_multitransformer.sh — build-test: k4FWCore MultiTransformer (multiple outputs, type aliases) +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyMultiTransformer \ + -i 'edm4hep::MCParticleCollection:InputParticles' \ + 'edm4hep::SimTrackerHitCollection:InputHits' \ + -o 'edm4hep::MCParticleCollection:OutputParticles' \ + 'podio::UserDataCollection:Counter' \ + --type-aliases \ + -p 'int:Offset:10:Integer to add to values' + +echo "PASS: multitransformer" diff --git a/k4FWCore/helpers/tests/test_producer.sh b/k4FWCore/helpers/tests/test_producer.sh new file mode 100644 index 00000000..02259052 --- /dev/null +++ b/k4FWCore/helpers/tests/test_producer.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +# test_producer.sh — build-test: k4FWCore Producer (single output, one property) +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyProducer \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + -p 'int:ExampleInt:3:An example integer property' + +echo "PASS: producer" diff --git a/k4FWCore/helpers/tests/test_runtime_consumer.sh b/k4FWCore/helpers/tests/test_runtime_consumer.sh new file mode 100644 index 00000000..ad11c54d --- /dev/null +++ b/k4FWCore/helpers/tests/test_runtime_consumer.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +# test_runtime_consumer.sh — build-test: k4FWCore Consumer with runtime (variable-length) inputs +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyRuntimeConsumer \ + -i 'edm4hep::MCParticleCollection:InputCollections' \ + --runtime-inputs 'edm4hep::MCParticleCollection:InputCollections:MCParticles0,MCParticles1' \ + --private-properties \ + -p 'int:Offset:10:Integer to add to values' + +echo "PASS: runtime_consumer" diff --git a/k4FWCore/helpers/tests/test_runtime_transformer.sh b/k4FWCore/helpers/tests/test_runtime_transformer.sh new file mode 100644 index 00000000..bdf4617e --- /dev/null +++ b/k4FWCore/helpers/tests/test_runtime_transformer.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +# test_runtime_transformer.sh — build-test: k4FWCore Transformer with runtime outputs +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyRuntimeTransformer \ + -i 'edm4hep::MCParticleCollection:InputCollections' \ + --runtime-inputs 'edm4hep::MCParticleCollection:InputCollections:MCParticles' \ + --runtime-outputs 'edm4hep::MCParticleCollection' \ + --private-properties \ + -p 'int:NumCollections:3:Number of output collections' + +echo "PASS: runtime_transformer" diff --git a/k4FWCore/helpers/tests/test_transformer.sh b/k4FWCore/helpers/tests/test_transformer.sh new file mode 100644 index 00000000..79bbcebc --- /dev/null +++ b/k4FWCore/helpers/tests/test_transformer.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +## +## Copyright (c) 2014-2024 Key4hep-Project. +## +## This file is part of Key4hep. +## See https://key4hep.github.io/key4hep-doc/ for further info. +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. +## + +# test_transformer.sh — build-test: k4FWCore Transformer (single in/out, private property) +source "$(dirname "${BASH_SOURCE[0]}")/_test_common.sh" + +run_cmake_build MyTransformer \ + -i 'edm4hep::MCParticleCollection:InputCollection' \ + -o 'edm4hep::MCParticleCollection:OutputCollection' \ + --private-properties \ + -p 'int:Offset:10:Integer to add to values' + +echo "PASS: transformer"