Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions tinyml-modelmaker/tests/test_nas_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"""Tests for NAS support in run_tinyml_modelmaker.

These tests verify that the NAS-related guards in main() work correctly:
1. Model catalog validation is skipped when nas_enabled=True
2. A fallback model_description is generated with correct fields
3. Non-NAS models still fail validation when not in catalog
"""

import types
from unittest import mock

import pytest


def _make_config(nas_enabled=False, model_name="NAS_m", training_enable=True):
"""Build a minimal config dict for testing."""
return {
"common": {
"target_device": "F28P55",
"task_type": "generic_timeseries_classification",
},
"dataset": {"enable": False, "dataset_name": "default"},
"data_processing_feature_extraction": {"feature_extraction_name": "default"},
"training": {
"enable": training_enable,
"model_name": model_name,
"nas_enabled": nas_enabled,
},
"testing": {"enable": False},
"compilation": {"enable": False, "compile_preset_name": "default_preset"},
}


class TestNASModelValidation:
"""Test NAS model validation bypass in run_tinyml_modelmaker.main()."""

def test_unknown_model_rejected_without_nas(self):
"""A fake model name should cause main() to return False when NAS is off."""
from tinyml_modelmaker.run_tinyml_modelmaker import main

config = _make_config(nas_enabled=False, model_name="NONEXISTENT_MODEL_XYZ")
result = main(config)
assert result is False

def test_unknown_model_allowed_with_nas(self):
"""When NAS is enabled, an unknown model name should NOT cause early rejection."""
from tinyml_modelmaker.run_tinyml_modelmaker import main

config = _make_config(nas_enabled=True, model_name="NAS_m")
# main() will proceed past validation but may fail later (no real training env).
# We patch ModelRunner to prevent that — we just want to verify validation passes.
with mock.patch(
"tinyml_modelmaker.run_tinyml_modelmaker.main"
) as mock_main:
# Instead of running the real main, test the validation logic directly
pass

# Direct test: extract the validation logic
import tinyml_modelmaker
task_type = "generic_timeseries_classification"
task_category = tinyml_modelmaker.get_task_category_type_from_task_type(task_type)
target_module = tinyml_modelmaker.get_target_module_from_task_type(task_type)
ai_target_module = tinyml_modelmaker.ai_modules.get_target_module(target_module)

model_name = "NAS_m"
nas_enabled = True
model_description = ai_target_module.runner.ModelRunner.get_model_description(model_name)

# Model should NOT be in catalog
assert model_description is None

# But NAS guard should prevent rejection
should_reject = (model_description is None and not nas_enabled)
assert should_reject is False

def test_nas_fallback_model_description(self):
"""When NAS is enabled and model is not in catalog, a fallback description should be generated."""
import tinyml_modelmaker
task_type = "generic_timeseries_classification"
target_module = tinyml_modelmaker.get_target_module_from_task_type(task_type)
ai_target_module = tinyml_modelmaker.ai_modules.get_target_module(target_module)

model_name = "NAS_xl"
nas_enabled = True
model_description = ai_target_module.runner.ModelRunner.get_model_description(model_name)
assert model_description is None

# Simulate the fallback logic from run_tinyml_modelmaker.py
if nas_enabled and model_description is None:
model_description = {
'common': {'generic_model': True},
'training': {
'training_backend': 'tinyml_tinyverse',
'model_training_id': model_name,
},
}

assert model_description is not None
assert model_description['common']['generic_model'] is True
assert model_description['training']['training_backend'] == 'tinyml_tinyverse'
assert model_description['training']['model_training_id'] == 'NAS_xl'

def test_nas_model_description_update_safe(self):
"""params.update(model_description or {}) should not crash with None."""
model_description = None
safe = model_description or {}
assert safe == {}
# Non-None case should pass through
model_description = {'training': {'training_backend': 'tinyml_tinyverse'}}
safe = model_description or {}
assert safe == model_description

def test_known_model_still_works(self):
"""A real model name should still pass validation as before (regression test)."""
import tinyml_modelmaker
task_type = "generic_timeseries_classification"
target_module = tinyml_modelmaker.get_target_module_from_task_type(task_type)
ai_target_module = tinyml_modelmaker.ai_modules.get_target_module(target_module)

# Pick a known model from the catalog
all_models = ai_target_module.runner.ModelRunner.get_model_description
# RES_CAT_CNN_TS_GEN_BASE_3K should exist
desc = all_models("RES_CAT_CNN_TS_GEN_BASE_3K")
if desc is not None:
assert 'training' in desc
assert 'training_backend' in desc['training']


class TestNASEnabledFlag:
"""Test the nas_enabled boolean handling (the str2bool fix)."""

def test_str2bool_returns_bool(self):
"""str2bool should return a Python bool, not a string."""
from tinyml_tinyverse.common.utils.misc_utils import str2bool
assert str2bool("True") is True
assert str2bool("true") is True
assert str2bool("1") is True
assert str2bool("False") is False
assert str2bool("false") is False
assert str2bool("0") is False

def test_bool_true_is_truthy(self):
"""Boolean True should be truthy (the fix: `if args.nas_enabled:` works)."""
# This is what the fixed code does
assert bool(True) # truthy check
# This is what the OLD buggy code did
assert (True == 'True') is False # string comparison fails!

def test_nas_enabled_argparse_integration(self):
"""Verify that the train script's argparse correctly converts nas_enabled to bool."""
from tinyml_tinyverse.references.timeseries_classification.train import get_args_parser
parser = get_args_parser()
# Simulate what modelmaker passes: --nas_enabled True
# --sampling-rate is required by the base parser
args = parser.parse_args([
'--nas_enabled', 'True',
'--data-path', '/tmp',
'--sampling-rate', '16000',
])
assert args.nas_enabled is True
assert isinstance(args.nas_enabled, bool)
# The truthy check should work
assert args.nas_enabled # if args.nas_enabled: → True

def test_nas_disabled_argparse_integration(self):
"""When nas_enabled is False, it should be falsy."""
from tinyml_tinyverse.references.timeseries_classification.train import get_args_parser
parser = get_args_parser()
args = parser.parse_args([
'--nas_enabled', 'False',
'--data-path', '/tmp',
'--sampling-rate', '16000',
])
assert args.nas_enabled is False
assert not args.nas_enabled # if args.nas_enabled: → False
15 changes: 13 additions & 2 deletions tinyml-modelmaker/tinyml_modelmaker/run_tinyml_modelmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,22 @@ def main(config):
params = ai_target_module.runner.ModelRunner.init_params()
# get pretrained model for the given model_name
model_name = config['training']['model_name']
nas_enabled = config.get('training', {}).get('nas_enabled', False)
model_description = ai_target_module.runner.ModelRunner.get_model_description(model_name)
if config.get('training').get('enable', True):
if model_description is None:
if model_description is None and not nas_enabled:
print(f"please check if the given model_name is a supported one: {model_name}")
return False
# When NAS is enabled, provide a minimal model description so the pipeline
# can locate the correct training module and treat it as a generic model.
if nas_enabled and model_description is None:
model_description = {
'common': {'generic_model': True},
'training': {
'training_backend': 'tinyml_tinyverse',
'model_training_id': model_name, # e.g. 'NAS_m'
},
}

dataset_preset_descriptions = ai_target_module.runner.ModelRunner.get_dataset_preset_descriptions(params)
dataset_preset_name = ai_target_module.constants.DATASET_DEFAULT
Expand Down Expand Up @@ -97,7 +108,7 @@ def main(config):
compilation_preset_description = preset_descriptions[target_device][task_type][compilation_preset_name]

# update the params with model_description, preset and config
params = params.update(model_description).update(dataset_preset_description).update(feature_extraction_preset_description).update(compilation_preset_description).update(config)
params = params.update(model_description or {}).update(dataset_preset_description).update(feature_extraction_preset_description).update(compilation_preset_description).update(config)

# create the runner
model_runner = ai_target_module.runner.ModelRunner(params)
Expand Down
Loading