Skip to content
Draft
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
4 changes: 4 additions & 0 deletions src/load/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Release History
===============

2.1.1
++++++
* Fix `az load test create`: `--test-id` is no longer required when `testId` is specified in the YAML configuration file passed via `--load-test-config-file`.

2.1.0
++++++
* Add option for `--autostop-engine-users` to set maximum users per engine for AutoStop criteria.
Expand Down
3 changes: 3 additions & 0 deletions src/load/azext_load/data_plane/load_test/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
type: command
short-summary: Create a new load test.
examples:
- name: Create a test with load test config file containing testId (--test-id can be omitted).
text: |
az load test create --load-test-resource sample-alt-resource --resource-group sample-rg --load-test-config-file ~/resources/sample-config.yaml
- name: Create a test with load test config file.
text: |
az load test create --load-test-resource sample-alt-resource --resource-group sample-rg --test-id sample-test-id --load-test-config-file ~/resources/sample-config.yaml
Expand Down
2 changes: 1 addition & 1 deletion src/load/azext_load/data_plane/utils/argtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
validator=validators.validate_test_id,
options_list=["--test-id", "-t"],
type=str,
help="Test ID of the load test",
help="Test ID of the load test. Required if not specified in the load test config file.",
)

test_run_id = CLIArgumentType(
Expand Down
1 change: 1 addition & 0 deletions src/load/azext_load/data_plane/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

@dataclass
class LoadTestConfigKeys:
TEST_ID = "testId"
DISPLAY_NAME = "displayName"
DESCRIPTION = "description"
TEST_PLAN = "testPlan"
Expand Down
14 changes: 13 additions & 1 deletion src/load/azext_load/data_plane/utils/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,19 @@ def _validate_id(namespace, id_name, arg_name=None):


def validate_test_id(namespace):
"""Validates test-id"""
"""Validates test-id. If test-id is not provided, tries to read it from the load test config file."""
if getattr(namespace, "test_id", None) is None:
load_test_config_file = getattr(namespace, "load_test_config_file", None)
if load_test_config_file and os.path.isfile(load_test_config_file):
try:
with open(load_test_config_file, "r", encoding="UTF-8") as f:
data = yaml.safe_load(f) or {}
from .constants import LoadTestConfigKeys
test_id_from_yaml = data.get(LoadTestConfigKeys.TEST_ID)
if test_id_from_yaml is not None:
namespace.test_id = str(test_id_from_yaml)
except Exception: # pylint: disable=broad-except
pass
_validate_id(namespace, "test_id", "test-id")


Expand Down
229 changes: 229 additions & 0 deletions src/load/azext_load/tests/latest/test_load_test_id_from_yaml_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import os
import tempfile
import unittest
import yaml


class SimpleNamespace:
"""Minimal namespace mock for validator tests."""
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)


def _get_validate_test_id():
"""Import validate_test_id directly from validators module."""
import ast
import types

validators_path = os.path.join(
os.path.dirname(__file__),
"..", "..", "data_plane", "utils", "validators.py"
)
with open(validators_path, "r", encoding="UTF-8") as f:
source = f.read()

# Extract only the functions we need without the full module-level imports
# by executing the full source in a sandboxed namespace with mocked imports
import sys
import re as re_mod

mock_azure_error = type(
"InvalidArgumentValueError", (Exception,), {}
)
mock_file_error = type("FileOperationError", (Exception,), {})

class MockLogger:
def debug(self, *a, **kw): pass
def info(self, *a, **kw): pass
def warning(self, *a, **kw): pass
def error(self, *a, **kw): pass

# Build fake modules to satisfy imports
import types as types_mod

def make_module(name, attrs=None):
mod = types_mod.ModuleType(name)
if attrs:
for k, v in attrs.items():
setattr(mod, k, v)
return mod

# Minimal stubs for modules used by validators
azure_cli_core_azclierror = make_module(
"azure.cli.core.azclierror",
{"InvalidArgumentValueError": mock_azure_error,
"FileOperationError": mock_file_error}
)
azure_cli_core_params = make_module(
"azure.cli.core.commands.parameters",
{"get_subscription_locations": lambda cmd: []}
)
azure_mgmt_core_tools = make_module(
"azure.mgmt.core.tools",
{"is_valid_resource_id": lambda rid: True}
)

# Import constants and models from real modules
import importlib.util

def import_from_path(mod_name, rel_path):
full_path = os.path.normpath(os.path.join(
os.path.dirname(validators_path), rel_path
))
spec = importlib.util.spec_from_file_location(mod_name, full_path)
mod = importlib.util.module_from_spec(spec)
return mod, spec

# We need constants and models available under .constants and .models
constants_mod, constants_spec = import_from_path(
"azext_load.data_plane.utils.constants",
"constants.py"
)
# constants.py imports models.py
models_path = os.path.normpath(os.path.join(
os.path.dirname(validators_path), "models.py"
))
models_spec_obj = importlib.util.spec_from_file_location(
"azext_load.data_plane.utils.models", models_path
)
models_mod = importlib.util.module_from_spec(models_spec_obj)
sys.modules["azext_load.data_plane.utils.models"] = models_mod
models_spec_obj.loader.exec_module(models_mod)
constants_spec.loader.exec_module(constants_mod)
sys.modules["azext_load.data_plane.utils.constants"] = constants_mod

# Stub out vendored sdk module
vendored_stub = make_module(
"azext_load.vendored_sdks.loadtesting.models",
{
"NotificationEventType": type("NotificationEventType", (), {}),
"TestRunStatus": type("TestRunStatus", (), {}),
"PassFailTestResult": type("PassFailTestResult", (), {}),
}
)
sys.modules.setdefault(
"azext_load.vendored_sdks.loadtesting.models", vendored_stub
)

# Stub utils (circular import avoidance)
utils_stub = make_module(
"azext_load.data_plane.utils.utils",
{}
)
sys.modules.setdefault(
"azext_load.data_plane.utils.utils", utils_stub
)

# Stub azure hierarchy
for mod_name in [
"azure", "azure.cli", "azure.cli.core",
"azure.cli.core.azclierror", "azure.cli.core.commands",
"azure.cli.core.commands.parameters",
"azure.mgmt", "azure.mgmt.core", "azure.mgmt.core.tools",
]:
sys.modules.setdefault(mod_name, make_module(mod_name))

sys.modules["azure.cli.core.azclierror"] = azure_cli_core_azclierror
sys.modules["azure.cli.core.commands.parameters"] = azure_cli_core_params
sys.modules["azure.mgmt.core.tools"] = azure_mgmt_core_tools

knack_log = make_module("knack.log", {"get_logger": lambda _: MockLogger()})
sys.modules.setdefault("knack", make_module("knack"))
sys.modules["knack.log"] = knack_log

# Stub utils module inline so '.' imports work
utils_inline = make_module(
"azext_load.data_plane.utils",
{"utils": utils_stub}
)
sys.modules.setdefault("azext_load.data_plane.utils", utils_inline)

# Now load validators
spec = importlib.util.spec_from_file_location(
"azext_load.data_plane.utils.validators", validators_path
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)

return mod.validate_test_id, mock_azure_error


class TestValidateTestIdFromYaml(unittest.TestCase):

def setUp(self):
self.validate_test_id, self.InvalidArgumentValueError = _get_validate_test_id()

def _make_yaml_file(self, content):
"""Write YAML content to a temp file and return its path."""
tmp = tempfile.NamedTemporaryFile(
mode="w", suffix=".yaml", delete=False, encoding="utf-8"
)
tmp.write(content)
tmp.flush()
tmp.close()
return tmp.name

def test_test_id_required_when_not_in_cli_or_yaml(self):
"""Error raised when test_id is absent from both CLI arg and YAML."""
ns = SimpleNamespace(test_id=None)
with self.assertRaises(self.InvalidArgumentValueError):
self.validate_test_id(ns)

def test_test_id_required_when_yaml_missing_key(self):
"""Error raised when YAML config file exists but has no testId key."""
yaml_file = self._make_yaml_file("displayName: mytest\n")
try:
ns = SimpleNamespace(test_id=None, load_test_config_file=yaml_file)
with self.assertRaises(self.InvalidArgumentValueError):
self.validate_test_id(ns)
finally:
os.unlink(yaml_file)

def test_test_id_read_from_yaml(self):
"""test_id is set from testId in YAML when CLI arg is absent."""
yaml_content = "testId: my-yaml-test-id\ndisplayName: mytest\n"
yaml_file = self._make_yaml_file(yaml_content)
try:
ns = SimpleNamespace(test_id=None, load_test_config_file=yaml_file)
self.validate_test_id(ns)
self.assertEqual(ns.test_id, "my-yaml-test-id")
finally:
os.unlink(yaml_file)

def test_cli_test_id_takes_precedence_over_yaml(self):
"""CLI-provided test_id is used even when YAML also has testId."""
yaml_content = "testId: yaml-test-id\ndisplayName: mytest\n"
yaml_file = self._make_yaml_file(yaml_content)
try:
ns = SimpleNamespace(test_id="cli-test-id", load_test_config_file=yaml_file)
self.validate_test_id(ns)
self.assertEqual(ns.test_id, "cli-test-id")
finally:
os.unlink(yaml_file)

def test_test_id_validates_format(self):
"""Invalid test_id value (uppercase) raises InvalidArgumentValueError."""
yaml_content = "testId: InvalidTestID\n"
yaml_file = self._make_yaml_file(yaml_content)
try:
ns = SimpleNamespace(test_id=None, load_test_config_file=yaml_file)
with self.assertRaises(self.InvalidArgumentValueError):
self.validate_test_id(ns)
finally:
os.unlink(yaml_file)

def test_valid_cli_test_id_passes(self):
"""A valid CLI test_id passes validation without error."""
ns = SimpleNamespace(test_id="valid-test-id-123")
self.validate_test_id(ns) # Should not raise
self.assertEqual(ns.test_id, "valid-test-id-123")


if __name__ == "__main__":
unittest.main()
Loading