Skip to content

test(integration): add end-to-end codelist extension test suite - #248

Open
jserrano-spec wants to merge 2 commits into
finos:mainfrom
jserrano-spec:feature/load-codelist-integration-test
Open

test(integration): add end-to-end codelist extension test suite#248
jserrano-spec wants to merge 2 commits into
finos:mainfrom
jserrano-spec:feature/load-codelist-integration-test

Conversation

@jserrano-spec

@jserrano-spec jserrano-spec commented Jul 17, 2026

Copy link
Copy Markdown

Description

Related Links

Overview

This PR introduces a comprehensive, end-to-end integration test suite to validate the new Codelist Extension mechanism. It provides concrete proof that the generated Python code successfully interacts with the CachedJSONRuneObjectDeserializer introduced in Runtime PR #33.

The test suite simulates a real-world distribution pipeline: generating code from a Rosetta model, packaging external JSON codelist data as a standalone Python wheel, and evaluating condition checkers directly through the native function registry.

Architecture & Changes

  • Rosetta Model (rune/test.rosetta): Introduces a minimal model defining a TradeTest type with [metadata scheme] annotations to trigger the generation of FpML codelist validation logic.

  • Mock Data Wheel (json/*): Creates a lightweight Python package (test_codelists_data) containing business-center.json. This successfully proves that the runtime's importlib.resources logic correctly resolves and reads JSON payloads distributed as zipped wheel data.

  • Orchestration Script (run_codelist_tests.sh): A bash orchestration script mirroring the repository's established testing conventions. It safely handles environmental scrubbing, Maven auto-building, code generation, wheel packaging, and dependency installation (fetching the targeted runtime branch).

  • Pytest Suite (tests/test_codelists.py): Explicitly wires the CachedJSONRuneObjectDeserializer to the generated codelist.functions.LoadCodeList native function. It asserts that ValidateFpMLCodingSchemeDomain accurately loads the mock JSON and correctly evaluates valid and invalid codes.

  • .gitignore: Refactored for better human readability and updated to exclude the ephemeral target/ and build/ artifacts generated by this specific test suite.

Testing Instructions

To execute the integration tests locally:

cd python-test/code-list-tests
./run_codelist_tests.sh

Adds a comprehensive integration test suite under `python-test/code-list-tests` to validate the new JSON codelist deserialization and native function registry mechanics from PR finos#33 in runtime repository.

Details:
- `rune/test.rosetta`: Minimal model defining a `TradeTest` type with `[metadata scheme]` annotations to trigger codelist generation.
- `json/*`: A mock Python package (`test_codelists_data`) containing `business-center.json` to prove `importlib.resources` successfully reads zipped wheel data.
- `run_codelist_tests.sh`: Orchestration script mirroring repository conventions to scrub the environment, auto-build the generator, generate test models, build local wheels, and execute Pytest.
- `tests/test_codelists.py`: Pytest suite explicitly validating `ValidateFpMLCodingSchemeDomain` via the `CachedJSONRuneObjectDeserializer` extension.
- `.gitignore`: Formatted for readability and updated to exclude `target/` and `build/` artifacts generated by this specific test suite.
@jserrano-spec jserrano-spec changed the title test(integration): add end-to-end codelist extension test suite Test(integration): Add end-to-end codelist extension test suite Jul 17, 2026
@jserrano-spec jserrano-spec changed the title Test(integration): Add end-to-end codelist extension test suite test(integration): add end-to-end codelist extension test suite Jul 17, 2026
@dschwartznyc

dschwartznyc commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

@jserrano-spec @plamen-neykov

Good progress on both the runtime and in defining the end to end tests.

These help clarify that there is still some ground to cover to provide Python support for code lists.

At a minimum the expectation should be that code list validation comes when executing the BaseClass function validate_model.

Specifically, we would like to replace:

    if hasattr(trade.businessCenter, "value"):
        assert trade.businessCenter.value == "GBLO"
    else:
        assert trade.businessCenter == "GBLO"

with

    assert trade.validate_model() == []  # empty list = no violations

At the moment, the validate_model function does not do anything for at least two Python ecosystem TypeAlias gaps:

  1. Lack of support for adding a condition to a TypeAlias.
  2. Lack of support for parametrizing the creation of a TypeAlias. I.E. typeAlias FpMLCodingScheme(domain string) is not supported.

Do we also have a simple way to initialize the code list perhaps by a predefined function that loads the domain based on a string?

Further, for model consistency there should ideally be validation when a value is assigned to a attribute of type FpMLCodingScheme even if this is inconsistent with the Java implementation. Since code lists are designed to selectively replace ENUMs, the goal should be to find issues as soon as possible in a like manner.

Additionally, I'd like to suggest a couple of changes to the test to better align with how the Generator repo is organized.

  1. Please generate the code to a directory under the project's target folder (ie target/python-code-list-test/). You can use this directory for the generated wheel as well whether in a build subdirectory or at the top. See the python-test/cdm-tests for an example.
  2. Rather than duplicating the rosetta code list definitions that are in CDM (rosetta-source/src/main/rosetta/base-staticdata-codelist-type.rosetta and rosetta-source/src/main/rosetta/base-staticdata-codelist-func.rosetta), i'd suggest that you copy both the type and function definition files directly into the test's rosetta source directory.

The test model would be reduced to:

import cdm.base.staticdata.codelist.FpMLCodingScheme
// ==============================================================================
// Our Integration Test Model
// ==============================================================================
typeAlias BusinessCenter: FpMLCodingScheme(domain: "business-center")

type TradeTest: <"A minimal type to test codelist integration and Pydantic validation.">
    businessCenter BusinessCenter (1..1)
        [metadata scheme]

…ture

Refactors the codelist integration tests based on PR feedback to align with the generator's global directory structure and native CDM definitions.

Details:
- `run_codelist_tests.sh`: Relocated generated artifacts and wheels to the global `target/python-code-list-test/` directory. Added cleanup logic for ephemeral `setuptools` build artifacts.
- `run_codelist_tests.sh`: Updated dependency installation to pull the upstream `finos/rune-python-runtime` main branch, leveraging the newly merged `CachedJSONRuneObjectDeserializer` (v2.1.0+).
- `rune/*`: Removed mocked codelist definitions from `test.rosetta`. Copied native `base-staticdata-codelist-type.rosetta` and `base-staticdata-codelist-func.rosetta` to serve as the deterministic source of truth.
- `business-center.json`: Enriched the mock payload with the required `identification` block and optional metadata to satisfy strict Pydantic validation against the true CDM schema.
- `test_codelists.py`: Updated namespace imports to reflect the true CDM structure. Replaced manual validation checks with `trade.validate_model() == []` (acting as a placeholder until upcoming runtime TypeAlias enhancements are implemented).
- `.gitignore`: Reverted custom integration test rules, as artifacts now live in the globally ignored `target/` directory.
@jserrano-spec
jserrano-spec force-pushed the feature/load-codelist-integration-test branch from 734529e to dd792da Compare July 22, 2026 13:24
@jserrano-spec

Copy link
Copy Markdown
Author

@dschwartznyc @plamen-neykov

Thanks for the feedback and architectural guidance!

I have just pushed a commit implementing the structural feedback and suggestions you outlined for this PR:

  • Target Directory: Moved the generated code and wheels to the global target/python-code-list-test/ directory to align with the cdm-tests structure.
  • Rosetta Files: Copied base-staticdata-codelist-type.rosetta and base-staticdata-codelist-func.rosetta directly into the test's rune/ folder, and stripped down test.rosetta to purely import the types.
  • Model Validation: Swapped the test assertion to use validate_model() exactly as requested (acting as a placeholder until TypeAlias support is added to the runtime).
  • Code Cleanup: Removed the .gitignore rules that targeted our local directory and added cleanup logic for ephemeral setuptools build artifacts in json/.
  • Target Runtime Repo: Updated the dependency installation to pull the upstream finos/rune-python-runtime main branch, leveraging the newly merged CachedJSONRuneObjectDeserializer (v2.1.0+).

Regarding the simple way to initialize the code list: I completely agree that requiring users to manually instantiate the provider and interact with the native registry is not a great developer experience. We can easily implement a helper function that accepts the target class, max cache size, and directory, initializes the deserializer, and registers it to the NATIVE_REGISTRY.

It would look something like this:

from pathlib import Path
from typing import Type
from rune.runtime.native_registry import rune_register_native
from rune.runtime.extensions.cached_json_rune_object_deserializer import CachedJSONRuneObjectDeserializer

def initialize_codelist_provider(codelist_dir: str | Path, target_class: Type, maxsize: int = 15):
    """
    Instantiates the JSON provider and binds it to the Rune native registry.
    """
    provider = CachedJSONRuneObjectDeserializer(
        codelist_dir=codelist_dir,
        target_class=target_class,
        maxsize=maxsize
    )
    
    rune_register_native("cdm.base.staticdata.codelist.functions.LoadCodeList", provider.load)

Architectural Question on Extensions: Now that we have opened the gate for future extensions of Rune-DSL models via the runtime, where do you envision this kind of initialization/injection code living? Should we group these initialization helpers inside a centralized dependency injection/bootstrap module, or should they remain isolated helper scripts within the extensions package?

I will follow up in a separate comment shortly to discuss the TypeAlias gap and the approach for triggering validation on assignment in more detail.

@dschwartznyc

dschwartznyc commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

@jserrano-spec - thanks for changes. Note that adding TypeAlias condition and input parameter support is likely to require changes to the generator.

Can we avoid initialization unless the developer wants to override defaults? In the case where the developer wants to use the defaults, can't we rely upon ValidateFpMLCodingSchemeDomain to load the code list once via the alias to LoadCodeList? Is it correct to assume that function searches for the relevant resource in the Python environment or in an appropriately placed file?

Also, can we simplify FpMLCodingScheme by removing the reference to ValidateFpMLCodingSchemeDomain?

current

typeAlias FpMLCodingScheme(domain string):
    string

    condition IsValidCodingScheme:
        if item exists
        then ValidateFpMLCodingSchemeDomain(item, domain)

replacement

typeAlias FpMLCodingScheme(domain string):
    string

    condition IsValidCodingScheme:
        if item exists
        then LoadCodeList(domain) -> codes -> value any = item

@jserrano-spec

Copy link
Copy Markdown
Author

@dschwartznyc @plamen-neykov

Apologies for the delay since my first message! As promised, here is the follow-up regarding the Runtime and Generator Core Changes.

Before deciding our next steps, I wanted to outline my understanding of the requirements, share what I’ve found while investigating the codebase and reading the official documentation, and propose a few potential solutions for your architectural assessment. I believe this will ensure we have a shared understanding of the issues and the path forward.

1. Understanding the Goal

To achieve parity with the Java implementation, we need two things:

  • Validation via validate_model() (requires updates to the rune-python-generator): Instead of manually checking if a code value is valid, we ideally want to use the runtime’s central validation function to evaluate codelists (and other typeAlias types) against their defined conditions.
  • Validation on Assignment (requires updates to the rune-python-generator): The system should "fail fast" and automate validation on assignment. It must trigger a validation error the exact moment an invalid value is assigned to an attribute (e.g., trade.businessCenter = "INVALID").

2. Investigation & Findings

While tracing how to implement this, I investigated the codebase and read RUNE_LANGUAGE_GAPS.md to understand the current limitations:

  • The TypeAlias Gap: It appears typeAlias definitions are currently stripped down to their base types (e.g., StrWithMeta) and any named condition blocks within them are discarded during generation. Only basic constraints like length and patterns are mapped to Pydantic Field arguments.

For example, in our integration test, the generator throws away the BusinessCenter type and the IsValidCodingScheme condition entirely:

  • Rosetta source:
typeAlias FpMLCodingScheme(domain string):
    string

    condition IsValidCodingScheme:
        if item exists
        then ValidateFpMLCodingSchemeDomain(item, domain)

typeAlias BusinessCenter: FpMLCodingScheme(domain: "business-center")

type TradeTest: <"A minimal type to test codelist integration.">
    businessCenter BusinessCenter (1..1)
        [metadata scheme]
  • Generated Python code (missing IsValidCodingScheme and BusinessCenter type):
class TradeTest(BaseDataClass):
    businessCenter: Annotated[StrWithMeta, StrWithMeta.serializer(), StrWithMeta.validator(('@scheme', ))] = Field(..., description='')
  • The Assignment Trigger: Inside the runtime, BaseDataClass currently manages reference replacements and Enum wrapping inside __setattr__, but it does not proactively trigger constraint validation upon reassignment.

3. Proposed Solutions for Assessment

Based on these findings and the RUNE_LANGUAGE_GAPS.md documentation, here are a couple of ways we could solve each issue:

For the TypeAlias Gap:

  • Option A (Lightweight): Update the Java generator to emit a constrained lightweight wrapper type using Pydantic’s Annotated, attaching the condition logic directly to the type hint:
# The generator outputs a custom Pydantic type with a validator attached
BusinessCenter = Annotated[str, AfterValidator(validate_fpml_coding_scheme)]

class TradeTest(BaseDataClass):
    businessCenter: BusinessCenter
  • Option B (Structural): Implement the broader "Rune Path" architecture. The generator stops flattening aliases into strings and generates them as full Python classes (inheriting from StrWithMeta) so they hold their own metadata and condition registries. validate_model() then evaluates them naturally:
class BusinessCenter(StrWithMeta):
    _FQRTN = 'cdm.base.staticdata.codelist.BusinessCenter'

    @rune_condition
    def condition_0_IsValidCodingScheme(self):
        return ValidateFpMLCodingSchemeDomain(self, "business-center")

For the Assignment Validation:

  • Option A (Native Pydantic): Enable Pydantic's native assignment validation within the model_config of BaseDataClass:
class BaseDataClass(BaseModel):
    model_config = ConfigDict(validate_assignment=True)
  • Option B (Custom Hook): Expand the existing __setattr__ method in BaseDataClass to manually intercept and evaluate Rune conditions for the specific field being updated:
def __setattr__(self, name, value):
    super().__setattr__(name, value)
    # Custom logic to trigger rune_conditions for 'name' here

I would be glad to try to help contribute to these enhancements in the generator and runtime if you think it would be useful! If so, I would be grateful to know which of the proposed solutions for each issue aligns best with the project's long-term architecture.

Please let me know your thoughts on these approaches.

@dschwartznyc

Copy link
Copy Markdown
Contributor

@jserrano-spec so that we can keep a clear record of the conversation about technical alternatives I'm going to move all of this to issue #150

Please use it to add the plan for automatically loading the relevant codelist - the issue is that loading an explicit codelist should only be a concern for developers who want to override defaults. Everyone else should get the default automatically

@plamen-neykov

plamen-neykov commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@jserrano-spec
A quick comment on the initialisation of the code-lists loader - we should generate the initialisation in the __init__.py of the main package being generated (see how native functions are initialised). This would allow for a default package name to be used. E.g. assuming the model is called <model>, the default loader initialisation will use something a package name like finos.<model>-codelists as a default package holding the codelists (which can be later overridden by the developer).

@dschwartznyc

Copy link
Copy Markdown
Contributor

Please use Issue #150 for on going technical design questions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants