Skip to content

Testing TaskTonic: Mastering the ttDistiller

TaskTonic Philosophy

Testing software is fundamental, but testing concurrent, asynchronous systems is notoriously difficult. In standard threaded or asyncio applications, you are at the mercy of the OS scheduler. A test might pass 99 times and fail on the 100th because a background thread executed a millisecond too late. You cannot easily pause time, inspect the exact state mid-execution, or predict the exact order of operations.

TaskTonic eliminates these threading nightmares through its atomic Sparkles and sequential queue. But how do you test an asynchronous queue without relying on messy time.sleep() calls in your tests?

Enter the ttDistiller: a specialized, deterministic test engine and an unparalleled documentation tool for TaskTonic.


1. The Print Statement vs. The Distiller (self.log vs ttDistiller)

When developing a Tonic, your first instinct is to use self.log("Doing something..."). This is excellent for visual debugging. You can watch the execution flow, state changes, and parameter values scroll by in your console.

However, you cannot write an automated unit test against console output. If someone breaks your Tonic's logic six months from now, self.log won't fail a CI/CD pipeline.

The ttDistiller replaces the standard ttCatalyst engine during testing. Instead of running a continuous, infinite loop in the background, the Distiller acts as a manual crank for the TaskTonic engine. It records every Sparkle, every state change, and every parameter passed, turning your asynchronous application into a completely synchronous, predictable, and heavily inspectable data structure.


2. Usage: Pytest or Standalone

You don't need a heavy testing framework to use the Distiller, but it integrates perfectly with them.

  • Standalone (Living Documentation & Prototyping): You can write a simple Python script that instantiates a Distiller and runs your Tonic. This approach is not just for assertions; it is absolutely perfect for generating living documentation. When trying to understand or tweak complex asynchronous flows, you can run a standalone script, copy the generated Markdown trace, and paste it into a viewer like markdownlivepreview.com. This gives you instant, visual feedback on the exact consequences of your code changes.
  • Pytest (Automated CI/CD): You can use pytest fixtures to set up your Distiller, run your Tonics through their paces, and assert conditions. The Distiller runs synchronously in the main thread, making your test suite incredibly fast and 100% deterministic.

3. Controlling Time and Execution: Distiller Functions

Because the Distiller owns the queue, you dictate exactly when and how Sparkles are executed. Every time you crank the engine, the Distiller returns a comprehensive trace dictionary of everything that happened.

Step-by-Step Sparkling

Instead of start_sparkling(), you tell the Distiller exactly how many items to process from the queue.

# Process exactly ONE Sparkle on the queue, then pause.
trace = distiller.sparkle(sparkle_count=1) 

# Process 5 Sparkles, then pause.
trace = distiller.sparkle(sparkle_count=5)
This allows you to freeze the universe mid-execution and inspect the state of your application.

Condition Triggers (Single-Tonic vs. Multi-Tonic)

Often, you don't know exactly how many Sparkles it takes to finish a complex network handshake. You just want to run the engine until a specific event happens.

A. Single-Tonic Tests (Direct Parameters) If you are unit testing an isolated Tonic, you can pass stop conditions directly as arguments. The Distiller will pause as soon as any active Tonic hits the requirement.

# Run the engine until the Tonic enters the 'finished' state
trace = distiller.sparkle(till_state_in=['finished'])

# Run the engine until a specific Sparkle method is called
trace = distiller.sparkle(till_sparkle_in=['ttse__on_data_received'])

B. Integration Tests (The contract Dictionary) When testing multiple Tonics interacting (e.g., a Client and a Server), the direct parameters fall short because you often need to wait until both systems reach a specific state. For this, you use the declarative contract dictionary.

The contract allows you to define strict AND/OR logic, assign independent conditions per Tonic, and "probe" internal variables without polluting the global trace. The Distiller uses a _freeze_value mechanism to safely create static snapshots of complex objects precisely before and after every Sparkle.

Contract Parameters Overview

Parameter Scope Description
timeout Global Maximum time in seconds before forcing a stop condition. Default is a high fallback value.
stop_match_count Global Tonics required to meet conditions to halt execution. Use 'all' (AND) or 1 (OR).
till_state_in Per-Tonic List of states. The Distiller stops when the Tonic transitions into one of these states.
till_sparkle_in Per-Tonic List of Sparkle methods. The Distiller stops after the specified Sparkle is executed.
probes Per-Tonic Variables to snapshot before and after every Sparkle, storing them in the trace log.
stop_on_probe Per-Tonic A dictionary (e.g., {'is_ready': True}). Halts execution if the probe matches this exact value.
# Tell the Distiller to stop ONLY when the Client is connected 
# AND the Server has registered exactly 1 active connection internally.
integration_contract = {
    'timeout': 5.0,
    'stop_match_count': 'all',  # 'all' = AND logic. Use '1' for OR logic.
    'tonics': {
        'ClientTonic': {
            'till_state_in': ['connected'],
            'probes': ['retry_attempts']  # Snapshot this variable on every sparkle
        },
        'ServerTonic': {
            'probes': ['active_connections'],
            'stop_on_probe': {'active_connections': 1} # Pause when probe hits this value
        }
    }
}

trace = distiller.sparkle(contract=integration_contract)

4. Unpacking the Status Trace (Assertions & Profiling)

Every time distiller.sparkle() finishes, it returns a rich status dictionary. This is your primary tool for writing assert statements in your tests.

The Root Trace Elements

The returned dictionary contains metadata about the execution run:

  • status: The final status of the engine ('running' or 'catalyst finished').
  • start@ / end@: Absolute timestamps of when the sparkle run started and ended.
  • stop_condition: A list explaining why the Distiller paused.
    • Values can include: 'timeout', 'sparkle_count', 'state_trigger: [state_name]', 'sparkle_trigger: [sparkle_name]', 'contract_met', or 'catalyst finished'.
  • contract_details: (Present only when a contract is met) A rich dictionary containing precise metadata about the matched conditions, including match_count, target_count, and a matched_tonics sub-dictionary detailing exactly which states, sparkles, or probes triggered the success.
  • sparkle_trace: A detailed list of every single Sparkle that was executed.

The sparkle_trace List

Each item in the sparkle_trace list is a dictionary describing a single atomic action:

  • id / tonic: The ID and name of the Tonic that executed the Sparkle.
  • sparkle: The exact method name (e.g., ttsc__process_chunk).
  • args / kwargs: The arguments passed into the Sparkle.
  • source: A tuple (source_tonic, source_sparkle_name, source_id) indicating who queued this Sparkle.
  • at_enter / at_exit: Sub-dictionaries capturing the exact state before and after the Sparkle. They contain:
    • @: The precise timestamp (useful for profiling).
    • state: The name of the Tonic's state machine state.
    • probes: A dictionary containing the frozen values of any requested probes for this specific Tonic.
    • sparkling: (Only in at_exit) Boolean indicating if the engine is still running.

Examples: Writing Assertions with the Trace

1. Asserting Stop Conditions: Ensure your logic stopped because it reached the desired state, not because it timed out.

# Using a contract for an integration test
trace = distiller.sparkle(contract={
    'timeout': 2.0,
    'stop_match_count': 'all',
    'tonics': {'ClientTonic': {'till_state_in': ['authenticated']}}
})

# Check that the distiller stopped successfully
assert 'contract_met' in trace['stop_condition']
assert 'timeout' not in trace['stop_condition']

# Inspect the exact reason the contract was met
details = trace['contract_details']
assert details['match_count'] == 1
assert "state: 'authenticated'" in details['matched_tonics']['ClientTonic']

2. Asserting Probed Data: Verify that internal variables were updated correctly during the Sparkle execution.

trace = distiller.sparkle(
    sparkle_count=1, 
    contract={
        'tonics': {
            'ClientTonic': {'probes': ['retry_attempts']}
        }
    }
)

# Grab the last executed sparkle from the trace
last_sparkle = trace['sparkle_trace'][-1]

# Check the probe values before and after the sparkle executed
assert last_sparkle['at_enter']['probes']['retry_attempts'] == 0
assert last_sparkle['at_exit']['probes']['retry_attempts'] == 1

3. Profiling (Finding the "Giant Sparkle"): Remember the golden rule of Sparkling Programming: Never block the Catalyst. If you suspect your UI is stuttering, you can check the Distiller's execution times.

trace = distiller.sparkle(till_state_in=['done'])

for action in trace['sparkle_trace']:
    enter_time = action['at_enter']['@']
    exit_time = action['at_exit']['@']
    duration_ms = (exit_time - enter_time) * 1000

    # Fail the test if any single Sparkle blocks the thread for more than 50ms!
    assert duration_ms < 50, f"Giant Sparkle detected: {action['sparkle']} took {duration_ms}ms"

5. Mocking Infusions vs. Integration Testing

TaskTonic encourages building hierarchical applications where a Parent Tonic delegates work to Child Tonics (Infusions). The Distiller allows you to test these at multiple levels.

A. The Unit Test (Mocking Infusions)

If you want to test the Parent's logic independently, you don't want the actual IP connections or databases (the children) to fire.

  1. Mock the Infusion: Override the child Tonic with a mock version.
  2. Inject Events: From your test script, you act as the child. You manually queue up events to see how the Parent reacts.
# We are the test. We pretend the child (network socket) received data.
# We inject this directly onto the queue for the parent to process.
parent_tonic.ttse__on_network_data({"status": "ok"})

# Process the injected event
trace = distiller.sparkle(sparkle_count=1)

# Verify the parent handled the mock data correctly
assert parent_tonic.get_current_state_name() == 'processing_data'

B. The Integration Test (Full Hierarchy)

You can also load the parent and all its real infusions into the Distiller.

Because all Tonics in the same Formula share the same Catalyst engine, the Distiller easily manages the master queue for the entire tree. You can trigger a high-level command on the Parent, tell the Distiller to run 50 Sparkles, and watch the entire cascade of commands and events flow down to the children and bubble back up to the parent. It provides a flawless, synchronous integration test of a highly asynchronous system.


6. Output & Visualization: Status vs. Print vs. Markdown

Once your Distiller has run, you have three primary ways to interact with the execution data, depending on your needs.

1. The Raw status Dictionary

This is the unadulterated dictionary returned by distiller.sparkle(). It contains every timestamp, trace entry, and probe mutation in a structured format. * Best for: Programmatic evaluation. You use this inside your test files to write assert statements and strictly validate logic, state changes, and stop conditions.

2. The stat_print() Method

A fast, lightweight console reporting tool. When you call distiller.stat_print(trace), it formats the raw status dictionary into a readable CLI output, showing relative execution times, state changes, and source bindings. * Best for: Live debugging during development. When a test suddenly fails, this provides immediate, human-readable terminal feedback to spot what went wrong without leaving your IDE console.

3. The Markdown Generator API (md_document)

A built-in suite of documentation tools (e.g., md_create, md_add_mermaid_flow, md_add_trace_table) that translates raw status dictionaries into comprehensive, visual Markdown files. By passing single statuses, lists of statuses, or dictionary mappings like {"Initialization": trace_1, "Failure Simulation": trace_2}, you can generate detailed reports complete with dynamically drawn Mermaid sequence diagrams, state visualizations, and performance tables. * Best for: Permanent test artifacts, CI/CD pipeline summaries, and team communication. It provides a visual, irrefutable map of exactly how your asynchronous system behaved during a specific run.


7. Example: End-to-End Test and Markdown Output

Below is an example of a simple standalone script that tests a HelloWorld Tonic. By chunking the execution into specific contracts and passing them to the Markdown generator, we create a step-by-step visual storyboard of the application's lifecycle.

The Python Test Script

from TaskTonic import *
from TaskTonic.ttTonicStore import ttDistillerFormula

class HelloWorld(ttTonic):
    def __init__(self, name=None, log_mode=None):
        super().__init__(name, log_mode)
        self.step_cnt = 0
        self.to_state('hello')

    def ttse__on_start(self):
        ttTimerRepeat(seconds=1.5, name='tm_step')

    def ttse_hello__on_enter(self):
        self.log('Hello world')

    def ttse_hello__on_tm_step(self, tinfo):
        self.step_cnt += 1
        self.to_state('welcome')

    def ttse_welcome__on_enter(self):
        self.log('Welcome to TaskTonic')

    def ttse_welcome__on_tm_step(self, tinfo):
        self.step_cnt += 1
        self.to_state('ending')

    def ttse_ending__on_tm_step(self, tinfo):
        self.step_cnt += 1
        self.ttsc__finish()


class myApp(ttDistillerFormula):
    def creating_formula(self):
        return (
            ('tasktonic/log/to', 'off'),
        )

    def creating_main_catalyst(self):
        self.dist = super().creating_main_catalyst()
        return self.dist

    def creating_starting_tonics(self):
        self.dut = HelloWorld(name='dut')


if __name__ == '__main__':
    app = myApp()

    distiller = app.dist
    dut = app.dut

    # Initialize the Markdown Report
    distiller.md_create(title="DISTILLER HELLO WORLD")

    distiller.md_add_txt("## Single run to finish")
    distiller.md_add_txt("Start app and run to the end. Watch trace for output.")
    distiller.md_add_ledger()

    trc1 = distiller.sparkle(contract={
        'timeout': 5.0,
        'tonics':{
            'dut': {
                'probes': ['step_cnt'],
                'till_sparkle_in': ['ttse__on_start'],
            },
        }
    })
    trc2 = distiller.sparkle(contract={
        'timeout': 5.0,
        'tonics':{
            'dut': {
                'probes': ['step_cnt'],
                'till_sparkle_in': ['ttse__on_enter'],
            },
        }
    })
    trc3 = distiller.sparkle(contract={
        'timeout': 5.0,
        'tonics':{
            'dut': {
                'probes': ['step_cnt'],
                'till_sparkle_in': ['ttse__on_enter'],
            },
        }
    })
    trc4 = distiller.sparkle(contract={
        'timeout': 5.0,
        'tonics':{
            'dut': {
                'probes': ['step_cnt'],
                'till_sparkle_in': ['ttse__on_enter'],
            },
        }
    })
    trc5 = distiller.sparkle(contract={
        'timeout': 5.0,
        'tonics': {
            'dut': {
                'probes': ['step_cnt']
            },
        }
    })

    trace = (
        ('Initialisation', trc1),
        ('Run till state is "hello"', trc2),
        ('Run till state is "welcome"', trc3),
        ('Run till state is "ending"', trc4),
        ('Finishing', trc5),
    )

    # Process Finish Cycle Results into MD
    distiller.md_add_mermaid_flow(trace)

    print(distiller.md_get_output())

    distiller.teardown_test_environment()

The Generated Markdown Output

DISTILLER HELLO WORLD

Single run to finish

Start app and run to the end. Watch trace for output.

Ledger Dump

Ledger dump
 - 00[tt_main_catalyst] <ttDistiller> catalyzing:[] 
 - 01[dut] <HelloWorld>

Execution Flow

sequenceDiagram
    autonumber
    actor Tester as Tester
    participant tt_main_catalyst as tt_main_catalyst
    participant dut as dut
    participant tm_step as tm_step

    Note over Tester, tm_step: Initialisation

    tt_main_catalyst->>tt_main_catalyst: _ttss__tonic_startup
    tt_main_catalyst->>tt_main_catalyst: ttse__on_start
    dut->>tt_main_catalyst: _ttss__add_tonic_to_catalyst
    dut->>dut: _ttss__tonic_startup
    dut->>tm_step: __init__
    dut->>dut: ttse__on_start
    Note over Tester, tm_step: Run till state is "hello"

    dut->>dut: _ttss__init_state
    dut->>dut: _ttinternal_state_change_to
    note over dut: state: -- -> hello
    dut->>dut: ttse__on_enter
    Note over Tester, tm_step: Run till state is "welcome"

    tm_step->>dut: ttse__on_tm_step
    note over dut: step_cnt: 0 -> 1
    dut->>dut: ttse__on_exit
    dut->>dut: _ttinternal_state_change_to
    note over dut: state: hello -> welcome
    dut->>dut: ttse__on_enter
    Note over Tester, tm_step: Run till state is "ending"

    tm_step->>dut: ttse__on_tm_step
    note over dut: step_cnt: 1 -> 2
    dut->>dut: ttse__on_exit
    dut->>dut: _ttinternal_state_change_to
    note over dut: state: welcome -> ending
    dut->>dut: ttse__on_enter
    Note over Tester, tm_step: Finishing

    tm_step->>dut: ttse__on_tm_step
    note over dut: step_cnt: 2 -> 3
    dut->>dut: ttsc__finish
    dut->>dut: ttse__on_start_finishing
    tm_step->>dut: _ttss__on_infusion_completed
    dut->>dut: _ttss__complete
    note over dut: state: ending -> --
    dut->>dut: ttse__on_finished
    dut->>dut: _ttss__on_completion
    dut->>tt_main_catalyst: _ttss__remove_tonic_from_catalyst
    tt_main_catalyst->>tt_main_catalyst: ttsc__finish
    tt_main_catalyst->>tt_main_catalyst: ttse__on_start_finishing
    tt_main_catalyst->>tt_main_catalyst: _ttss__complete
    tt_main_catalyst->>tt_main_catalyst: ttse__on_finished
    tt_main_catalyst->>tt_main_catalyst: _ttss__on_completion