swaggerforge 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {swaggerforge-0.1.0/src/swaggerforge.egg-info → swaggerforge-0.2.0}/PKG-INFO +35 -7
  2. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/README.md +31 -5
  3. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/pyproject.toml +3 -3
  4. swaggerforge-0.2.0/src/swaggerforge/__main__.py +5 -0
  5. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge/cli.py +29 -6
  6. swaggerforge-0.2.0/src/swaggerforge/config.py +102 -0
  7. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge/output.py +2 -2
  8. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge/template.py +10 -5
  9. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge/templates/test_file.py.j2 +6 -1
  10. {swaggerforge-0.1.0 → swaggerforge-0.2.0/src/swaggerforge.egg-info}/PKG-INFO +35 -7
  11. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge.egg-info/SOURCES.txt +2 -0
  12. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge.egg-info/requires.txt +5 -1
  13. swaggerforge-0.2.0/tests/test_config.py +120 -0
  14. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/tests/test_template.py +24 -0
  15. swaggerforge-0.1.0/src/swaggerforge/__main__.py +0 -0
  16. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/LICENSE +0 -0
  17. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/setup.cfg +0 -0
  18. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge/__init__.py +0 -0
  19. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge/generator.py +0 -0
  20. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge/models.py +0 -0
  21. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge/parser.py +0 -0
  22. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge/validator.py +0 -0
  23. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge.egg-info/dependency_links.txt +0 -0
  24. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge.egg-info/entry_points.txt +0 -0
  25. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/src/swaggerforge.egg-info/top_level.txt +0 -0
  26. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/tests/test_generator.py +0 -0
  27. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/tests/test_output.py +0 -0
  28. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/tests/test_parser.py +0 -0
  29. {swaggerforge-0.1.0 → swaggerforge-0.2.0}/tests/test_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swaggerforge
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Automatic pytest test generation from OpenAPI (Swagger) specifications
5
5
  Author: Viktor Pylypenko
6
6
  License-Expression: MIT
@@ -10,14 +10,16 @@ Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: click>=8.0
12
12
  Requires-Dist: prance>=23.6.21.0
13
- Requires-Dist: openapi-core>=0.18
14
13
  Requires-Dist: openapi-spec-validator>=0.7
15
14
  Requires-Dist: jinja2>=3.0
16
15
  Requires-Dist: requests>=2.28
17
16
  Requires-Dist: jsonschema>=4.0
17
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
18
18
  Provides-Extra: dev
19
19
  Requires-Dist: pytest>=9.0; extra == "dev"
20
20
  Requires-Dist: flake8>=7.0; extra == "dev"
21
+ Requires-Dist: pytest-cov>=7.0; extra == "dev"
22
+ Requires-Dist: tox>=4.0; extra == "dev"
21
23
  Dynamic: license-file
22
24
 
23
25
  # SwaggerForge
@@ -45,6 +47,7 @@ established test-design techniques.
45
47
  - **Boolean coverage** >>> exercises both `true` and `false` for boolean fields
46
48
  - Deterministic output: the same specification always produces identical tests
47
49
  - Generated files use session-scoped pytest fixtures and run with no manual edits
50
+ - Optional `swaggerforge.toml` config file for project-level defaults
48
51
 
49
52
  ## Requirements
50
53
 
@@ -75,11 +78,36 @@ pytest tests_generated
75
78
 
76
79
  ### Options
77
80
 
78
- | Option | Description | Default |
79
- |------------|----------------------------------------------------|--------------------|
80
- | `--spec` | Path to the OpenAPI specification (JSON or YAML) | *(required)* |
81
- | `--url` | Base URL of the API under test | *(required)* |
82
- | `--output` | Directory for the generated test files | `tests_generated` |
81
+ | Option | Description | Default |
82
+ |------------|----------------------------------------------------|-----------------------------------|
83
+ | `--spec` | Path to the OpenAPI specification (JSON or YAML) | *(required)* |
84
+ | `--url` | Base URL of the API under test | *(required unless in config)* |
85
+ | `--output` | Directory for the generated test files | `tests_generated` |
86
+ | `--config` | Path to a configuration file | `./swaggerforge.toml` if present |
87
+
88
+ ## Configuration
89
+
90
+ Options that stay the same across runs can be kept in a `swaggerforge.toml`
91
+ file instead of being passed on the command line. The file is picked up
92
+ automatically from the directory where the tool is run, or an explicit path
93
+ can be given with `--config`.
94
+
95
+ ```toml
96
+ # swaggerforge.toml
97
+ base_url = "http://localhost:8080"
98
+ output_dir = "tests_generated"
99
+ timeout = 30
100
+ ```
101
+
102
+ | Key | Type | Effect |
103
+ |--------------|---------|---------------------------------------------------------------|
104
+ | `base_url` | string | Base URL of the API; makes `--url` optional |
105
+ | `output_dir` | string | Directory for generated files |
106
+ | `timeout` | integer | Embeds `timeout=<n>` into every generated HTTP request call |
107
+
108
+ Values given on the command line always take precedence over the config file.
109
+ Without a `timeout`, generated tests place no time limit on requests -
110
+ setting one makes test runs fail fast when the API is unreachable.
83
111
 
84
112
  ## How it works
85
113
 
@@ -23,6 +23,7 @@ established test-design techniques.
23
23
  - **Boolean coverage** >>> exercises both `true` and `false` for boolean fields
24
24
  - Deterministic output: the same specification always produces identical tests
25
25
  - Generated files use session-scoped pytest fixtures and run with no manual edits
26
+ - Optional `swaggerforge.toml` config file for project-level defaults
26
27
 
27
28
  ## Requirements
28
29
 
@@ -53,11 +54,36 @@ pytest tests_generated
53
54
 
54
55
  ### Options
55
56
 
56
- | Option | Description | Default |
57
- |------------|----------------------------------------------------|--------------------|
58
- | `--spec` | Path to the OpenAPI specification (JSON or YAML) | *(required)* |
59
- | `--url` | Base URL of the API under test | *(required)* |
60
- | `--output` | Directory for the generated test files | `tests_generated` |
57
+ | Option | Description | Default |
58
+ |------------|----------------------------------------------------|-----------------------------------|
59
+ | `--spec` | Path to the OpenAPI specification (JSON or YAML) | *(required)* |
60
+ | `--url` | Base URL of the API under test | *(required unless in config)* |
61
+ | `--output` | Directory for the generated test files | `tests_generated` |
62
+ | `--config` | Path to a configuration file | `./swaggerforge.toml` if present |
63
+
64
+ ## Configuration
65
+
66
+ Options that stay the same across runs can be kept in a `swaggerforge.toml`
67
+ file instead of being passed on the command line. The file is picked up
68
+ automatically from the directory where the tool is run, or an explicit path
69
+ can be given with `--config`.
70
+
71
+ ```toml
72
+ # swaggerforge.toml
73
+ base_url = "http://localhost:8080"
74
+ output_dir = "tests_generated"
75
+ timeout = 30
76
+ ```
77
+
78
+ | Key | Type | Effect |
79
+ |--------------|---------|---------------------------------------------------------------|
80
+ | `base_url` | string | Base URL of the API; makes `--url` optional |
81
+ | `output_dir` | string | Directory for generated files |
82
+ | `timeout` | integer | Embeds `timeout=<n>` into every generated HTTP request call |
83
+
84
+ Values given on the command line always take precedence over the config file.
85
+ Without a `timeout`, generated tests place no time limit on requests -
86
+ setting one makes test runs fail fast when the API is unreachable.
61
87
 
62
88
  ## How it works
63
89
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swaggerforge"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Automatic pytest test generation from OpenAPI (Swagger) specifications"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -16,11 +16,11 @@ keywords = ["openapi", "swagger", "pytest", "test generation", "api testing"]
16
16
  dependencies = [
17
17
  "click>=8.0",
18
18
  "prance>=23.6.21.0",
19
- "openapi-core>=0.18",
20
19
  "openapi-spec-validator>=0.7",
21
20
  "jinja2>=3.0",
22
21
  "requests>=2.28",
23
22
  "jsonschema>=4.0",
23
+ 'tomli>=2.0; python_version < "3.11"',
24
24
  ]
25
25
 
26
26
  [project.scripts]
@@ -38,4 +38,4 @@ addopts = "--import-mode=importlib"
38
38
  testpaths = ["tests"]
39
39
 
40
40
  [project.optional-dependencies]
41
- dev = ["pytest>=9.0", "flake8>=7.0"]
41
+ dev = ["pytest>=9.0", "flake8>=7.0", "pytest-cov>=7.0", "tox>=4.0"]
@@ -0,0 +1,5 @@
1
+ """Entry point for running SwaggerForge as a module (python -m swaggerforge)."""
2
+ from swaggerforge.cli import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -1,6 +1,7 @@
1
1
  """Command-line interface for SwaggerForge."""
2
2
 
3
3
  import click
4
+ from swaggerforge.config import load_config, ConfigError
4
5
  from swaggerforge.validator import load_and_validate, SpecValidationError
5
6
  from swaggerforge.parser import parse_spec, SpecParseError
6
7
  from swaggerforge.generator import generate_scenarios
@@ -23,17 +24,39 @@ def main():
23
24
  )
24
25
  @click.option(
25
26
  "--url",
26
- required=True,
27
- help="Base URL of the API to be tested.",
27
+ default=None,
28
+ help="Base URL of the API to be tested. "
29
+ "Required unless base_url is set in the config file.",
28
30
  )
29
31
  @click.option(
30
32
  "--output",
31
- default="tests_generated",
33
+ default=None,
32
34
  type=click.Path(),
33
- help="Directory where generated test files will be written.",
35
+ help="Directory where generated test files will be written "
36
+ "[default: tests_generated].",
34
37
  )
35
- def generate(spec, url, output):
38
+ @click.option(
39
+ "--config",
40
+ "config_path",
41
+ default=None,
42
+ type=click.Path(),
43
+ help="Path to a swaggerforge.toml config file "
44
+ "[default: ./swaggerforge.toml if present].",
45
+ )
46
+ def generate(spec, url, output, config_path):
36
47
  """Generate pytest test files from an OpenAPI specification."""
48
+ try:
49
+ config = load_config(config_path)
50
+ except ConfigError as error:
51
+ raise click.ClickException(str(error))
52
+
53
+ url = url or config.base_url
54
+ if url is None:
55
+ raise click.ClickException(
56
+ "No base URL given: pass --url or set base_url in swaggerforge.toml."
57
+ )
58
+ output = output or config.output_dir or "tests_generated"
59
+
37
60
  click.echo(f"Reading specification: {spec}")
38
61
 
39
62
  try:
@@ -56,7 +79,7 @@ def generate(spec, url, output):
56
79
  click.echo(f"Generated {len(scenarios)} test scenarios.")
57
80
 
58
81
  try:
59
- written = write_test_files(scenarios, url, output)
82
+ written = write_test_files(scenarios, url, output, config.timeout)
60
83
  except OSError as error:
61
84
  raise click.ClickException(
62
85
  f"Could not write test files to '{output}': {error}"
@@ -0,0 +1,102 @@
1
+ """Loading of user configuration from a swaggerforge.toml file.
2
+
3
+ This module reads optional user configuration for the generator. An
4
+ explicitly given path must exist and parse; an auto-discovered file
5
+ (./swaggerforge.toml in the working directory) may be absent, in which
6
+ case an empty configuration is returned.
7
+ """
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ try:
13
+ import tomllib # Python 3.11+
14
+ except ModuleNotFoundError:
15
+ import tomli as tomllib # Python 3.10 backport
16
+
17
+ DEFAULT_CONFIG_FILENAME = "swaggerforge.toml"
18
+
19
+ _KNOWN_KEYS = {
20
+ "timeout": int,
21
+ "output_dir": str,
22
+ "base_url": str,
23
+ }
24
+
25
+
26
+ class ConfigError(Exception):
27
+ """Raised when a config file is missing (if explicitly requested) or invalid."""
28
+
29
+
30
+ @dataclass
31
+ class Config:
32
+ """User configuration loaded from swaggerforge.toml.
33
+
34
+ A field value of None means the setting was not present in the file.
35
+ """
36
+ timeout: int | None = None
37
+ output_dir: str | None = None
38
+ base_url: str | None = None
39
+
40
+
41
+ def load_config(config_path=None):
42
+ """Load configuration from a swaggerforge.toml file.
43
+
44
+ Args:
45
+ config_path: Explicit path to a config file. If given, the file
46
+ must exist and parse; otherwise ./swaggerforge.toml is
47
+ auto-discovered, and its absence yields an empty Config.
48
+
49
+ Returns:
50
+ A Config object with values from the file, or an empty Config
51
+ when no file was found during auto-discovery.
52
+
53
+ Raises:
54
+ ConfigError: If an explicitly given file is missing, if any file
55
+ contains invalid TOML, or if it contains unknown keys or
56
+ values of the wrong type.
57
+ """
58
+ if config_path is not None:
59
+ path = Path(config_path)
60
+ if not path.is_file():
61
+ raise ConfigError(f"Config file not found: '{config_path}'")
62
+ else:
63
+ path = Path(DEFAULT_CONFIG_FILENAME)
64
+ if not path.is_file():
65
+ return Config()
66
+
67
+ try:
68
+ raw = path.read_bytes()
69
+ # utf-8-sig strips a leading UTF-8 BOM if present (common on
70
+ # Windows) and is identical to utf-8 otherwise.
71
+ data = tomllib.loads(raw.decode("utf-8-sig"))
72
+ except tomllib.TOMLDecodeError as error:
73
+ raise ConfigError(f"Invalid config file '{path}': {error}")
74
+ except UnicodeDecodeError:
75
+ raise ConfigError(
76
+ f"Invalid config file '{path}': not valid UTF-8 "
77
+ f"(TOML files must be UTF-8 encoded; on Windows, beware that "
78
+ f"PowerShell redirection may create UTF-16 files)"
79
+ )
80
+
81
+ return _validate(data, path)
82
+
83
+
84
+ def _validate(data, path):
85
+ """Check keys and value types, returning a populated Config."""
86
+ unknown = set(data) - set(_KNOWN_KEYS)
87
+ if unknown:
88
+ names = ", ".join(sorted(unknown))
89
+ raise ConfigError(f"Unknown key(s) in config file '{path}': {names}")
90
+
91
+ for key, expected_type in _KNOWN_KEYS.items():
92
+ if key not in data:
93
+ continue
94
+ value = data[key]
95
+ # bool is a subclass of int in Python; reject it for timeout.
96
+ if isinstance(value, bool) or not isinstance(value, expected_type):
97
+ raise ConfigError(
98
+ f"Invalid value for '{key}' in config file '{path}': "
99
+ f"expected {expected_type.__name__}, got {type(value).__name__}"
100
+ )
101
+
102
+ return Config(**data)
@@ -12,7 +12,7 @@ from pathlib import Path
12
12
  from swaggerforge.template import render_test_file
13
13
 
14
14
 
15
- def write_test_files(scenarios, base_url, output_dir):
15
+ def write_test_files(scenarios, base_url, output_dir, timeout=None):
16
16
  """Group scenarios by tag, render them, and write one file per tag.
17
17
 
18
18
  Args:
@@ -32,7 +32,7 @@ def write_test_files(scenarios, base_url, output_dir):
32
32
  for tag in sorted(grouped):
33
33
  filename = f"test_{_sanitize(tag)}.py"
34
34
  file_path = output_path / filename
35
- content = render_test_file(filename, base_url, grouped[tag])
35
+ content = render_test_file(filename, base_url, grouped[tag], timeout)
36
36
  file_path.write_text(content, encoding="utf-8")
37
37
  written.append(str(file_path))
38
38
 
@@ -17,19 +17,21 @@ _env = Environment(
17
17
  )
18
18
 
19
19
 
20
- def render_test_file(filename, base_url, scenarios):
20
+ def render_test_file(filename, base_url, scenarios, timeout=None):
21
21
  """Render a list of scenarios into the text of a pytest test file.
22
22
 
23
23
  Args:
24
24
  filename: The name of the file being generated (e.g. 'test_pet.py').
25
25
  base_url: The base URL of the API under test.
26
26
  scenarios: A list of TestScenario objects for one resource tag.
27
+ timeout: Optional request timeout in seconds; when given, it is
28
+ embedded into every generated HTTP call.
27
29
 
28
30
  Returns:
29
31
  The rendered file content as a string.
30
32
  """
31
33
  template = _env.get_template("test_file.py.j2")
32
- rendered_scenarios = [_prepare_scenario(s) for s in scenarios]
34
+ rendered_scenarios = [_prepare_scenario(s, timeout) for s in scenarios]
33
35
  return template.render(
34
36
  filename=filename,
35
37
  base_url=base_url,
@@ -37,12 +39,12 @@ def render_test_file(filename, base_url, scenarios):
37
39
  )
38
40
 
39
41
 
40
- def _prepare_scenario(scenario):
42
+ def _prepare_scenario(scenario, timeout=None):
41
43
  """Pre-compute all values the template needs for one scenario."""
42
44
  return {
43
45
  "function_name": build_function_name(scenario),
44
46
  "docstring": scenario.description,
45
- "request_call": build_request_call(scenario),
47
+ "request_call": build_request_call(scenario, timeout),
46
48
  "expected_status": _status_codes_literal(scenario),
47
49
  "response_schema": _response_schema_literal(scenario),
48
50
  }
@@ -81,16 +83,19 @@ def _path_to_identifier(method, path):
81
83
  return f"{method}_{cleaned}"
82
84
 
83
85
 
84
- def build_request_call(scenario):
86
+ def build_request_call(scenario, timeout=None):
85
87
  """Assemble the full requests.<method>(...) call as a string."""
86
88
  endpoint = scenario.endpoint
87
89
  url = _build_url(endpoint.path, scenario.path_params)
88
90
 
89
91
  parts = [f'f"{{base_url}}{url}"']
92
+ parts.append("headers=headers")
90
93
  if scenario.query_params:
91
94
  parts.append(f"params={scenario.query_params!r}")
92
95
  if scenario.request_body is not None:
93
96
  parts.append(f"json={scenario.request_body!r}")
97
+ if timeout is not None:
98
+ parts.append(f"timeout={timeout}")
94
99
 
95
100
  args = ", ".join(parts)
96
101
  return f"requests.{endpoint.method}({args})"
@@ -11,9 +11,14 @@ import jsonschema
11
11
  def base_url():
12
12
  return "{{ base_url }}"
13
13
 
14
+
15
+ @pytest.fixture(scope="session")
16
+ def headers():
17
+ return {"Content-Type": "application/json"}
18
+
14
19
  {% for scenario in scenarios %}
15
20
 
16
- def {{ scenario.function_name }}(base_url):
21
+ def {{ scenario.function_name }}(base_url, headers):
17
22
  """{{ scenario.docstring }}"""
18
23
  response = {{ scenario.request_call }}
19
24
  assert response.status_code in {{ scenario.expected_status }}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swaggerforge
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Automatic pytest test generation from OpenAPI (Swagger) specifications
5
5
  Author: Viktor Pylypenko
6
6
  License-Expression: MIT
@@ -10,14 +10,16 @@ Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
11
  Requires-Dist: click>=8.0
12
12
  Requires-Dist: prance>=23.6.21.0
13
- Requires-Dist: openapi-core>=0.18
14
13
  Requires-Dist: openapi-spec-validator>=0.7
15
14
  Requires-Dist: jinja2>=3.0
16
15
  Requires-Dist: requests>=2.28
17
16
  Requires-Dist: jsonschema>=4.0
17
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
18
18
  Provides-Extra: dev
19
19
  Requires-Dist: pytest>=9.0; extra == "dev"
20
20
  Requires-Dist: flake8>=7.0; extra == "dev"
21
+ Requires-Dist: pytest-cov>=7.0; extra == "dev"
22
+ Requires-Dist: tox>=4.0; extra == "dev"
21
23
  Dynamic: license-file
22
24
 
23
25
  # SwaggerForge
@@ -45,6 +47,7 @@ established test-design techniques.
45
47
  - **Boolean coverage** >>> exercises both `true` and `false` for boolean fields
46
48
  - Deterministic output: the same specification always produces identical tests
47
49
  - Generated files use session-scoped pytest fixtures and run with no manual edits
50
+ - Optional `swaggerforge.toml` config file for project-level defaults
48
51
 
49
52
  ## Requirements
50
53
 
@@ -75,11 +78,36 @@ pytest tests_generated
75
78
 
76
79
  ### Options
77
80
 
78
- | Option | Description | Default |
79
- |------------|----------------------------------------------------|--------------------|
80
- | `--spec` | Path to the OpenAPI specification (JSON or YAML) | *(required)* |
81
- | `--url` | Base URL of the API under test | *(required)* |
82
- | `--output` | Directory for the generated test files | `tests_generated` |
81
+ | Option | Description | Default |
82
+ |------------|----------------------------------------------------|-----------------------------------|
83
+ | `--spec` | Path to the OpenAPI specification (JSON or YAML) | *(required)* |
84
+ | `--url` | Base URL of the API under test | *(required unless in config)* |
85
+ | `--output` | Directory for the generated test files | `tests_generated` |
86
+ | `--config` | Path to a configuration file | `./swaggerforge.toml` if present |
87
+
88
+ ## Configuration
89
+
90
+ Options that stay the same across runs can be kept in a `swaggerforge.toml`
91
+ file instead of being passed on the command line. The file is picked up
92
+ automatically from the directory where the tool is run, or an explicit path
93
+ can be given with `--config`.
94
+
95
+ ```toml
96
+ # swaggerforge.toml
97
+ base_url = "http://localhost:8080"
98
+ output_dir = "tests_generated"
99
+ timeout = 30
100
+ ```
101
+
102
+ | Key | Type | Effect |
103
+ |--------------|---------|---------------------------------------------------------------|
104
+ | `base_url` | string | Base URL of the API; makes `--url` optional |
105
+ | `output_dir` | string | Directory for generated files |
106
+ | `timeout` | integer | Embeds `timeout=<n>` into every generated HTTP request call |
107
+
108
+ Values given on the command line always take precedence over the config file.
109
+ Without a `timeout`, generated tests place no time limit on requests -
110
+ setting one makes test runs fail fast when the API is unreachable.
83
111
 
84
112
  ## How it works
85
113
 
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  src/swaggerforge/__init__.py
5
5
  src/swaggerforge/__main__.py
6
6
  src/swaggerforge/cli.py
7
+ src/swaggerforge/config.py
7
8
  src/swaggerforge/generator.py
8
9
  src/swaggerforge/models.py
9
10
  src/swaggerforge/output.py
@@ -17,6 +18,7 @@ src/swaggerforge.egg-info/entry_points.txt
17
18
  src/swaggerforge.egg-info/requires.txt
18
19
  src/swaggerforge.egg-info/top_level.txt
19
20
  src/swaggerforge/templates/test_file.py.j2
21
+ tests/test_config.py
20
22
  tests/test_generator.py
21
23
  tests/test_output.py
22
24
  tests/test_parser.py
@@ -1,11 +1,15 @@
1
1
  click>=8.0
2
2
  prance>=23.6.21.0
3
- openapi-core>=0.18
4
3
  openapi-spec-validator>=0.7
5
4
  jinja2>=3.0
6
5
  requests>=2.28
7
6
  jsonschema>=4.0
8
7
 
8
+ [:python_version < "3.11"]
9
+ tomli>=2.0
10
+
9
11
  [dev]
10
12
  pytest>=9.0
11
13
  flake8>=7.0
14
+ pytest-cov>=7.0
15
+ tox>=4.0
@@ -0,0 +1,120 @@
1
+ """Tests for the config module."""
2
+
3
+ import pytest
4
+
5
+ from swaggerforge.config import Config, ConfigError, load_config
6
+
7
+
8
+ VALID_TOML = """\
9
+ timeout = 30
10
+ output_dir = "my_tests"
11
+ base_url = "http://localhost:8080"
12
+ """
13
+
14
+
15
+ def test_load_explicit_valid_file(tmp_path):
16
+ """An explicitly given, valid config file is loaded fully."""
17
+ config_file = tmp_path / "custom.toml"
18
+ config_file.write_text(VALID_TOML, encoding="utf-8")
19
+
20
+ config = load_config(str(config_file))
21
+
22
+ assert config.timeout == 30
23
+ assert config.output_dir == "my_tests"
24
+ assert config.base_url == "http://localhost:8080"
25
+
26
+
27
+ def test_load_explicit_missing_file_raises(tmp_path):
28
+ """An explicitly given path that does not exist is an error."""
29
+ missing = tmp_path / "nope.toml"
30
+
31
+ with pytest.raises(ConfigError):
32
+ load_config(str(missing))
33
+
34
+
35
+ def test_load_explicit_malformed_file_raises(tmp_path):
36
+ """An explicitly given file with invalid TOML is an error."""
37
+ config_file = tmp_path / "broken.toml"
38
+ config_file.write_text("]] this is not toml [[", encoding="utf-8")
39
+
40
+ with pytest.raises(ConfigError):
41
+ load_config(str(config_file))
42
+
43
+
44
+ def test_autodiscovery_finds_file_in_cwd(tmp_path, monkeypatch):
45
+ """Without an explicit path, ./swaggerforge.toml is discovered."""
46
+ (tmp_path / "swaggerforge.toml").write_text(VALID_TOML, encoding="utf-8")
47
+ monkeypatch.chdir(tmp_path)
48
+
49
+ config = load_config()
50
+
51
+ assert config.timeout == 30
52
+ assert config.output_dir == "my_tests"
53
+ assert config.base_url == "http://localhost:8080"
54
+
55
+
56
+ def test_autodiscovery_absent_returns_empty_config(tmp_path, monkeypatch):
57
+ """No explicit path and no discoverable file yields an empty config."""
58
+ monkeypatch.chdir(tmp_path)
59
+
60
+ config = load_config()
61
+
62
+ assert config == Config()
63
+
64
+
65
+ def test_autodiscovery_malformed_raises(tmp_path, monkeypatch):
66
+ """A discovered file that exists but is invalid TOML is an error."""
67
+ (tmp_path / "swaggerforge.toml").write_text("]] broken [[", encoding="utf-8")
68
+ monkeypatch.chdir(tmp_path)
69
+
70
+ with pytest.raises(ConfigError):
71
+ load_config()
72
+
73
+
74
+ def test_partial_config_leaves_other_fields_none(tmp_path):
75
+ """Keys absent from the file stay None (unset)."""
76
+ config_file = tmp_path / "partial.toml"
77
+ config_file.write_text("timeout = 10\n", encoding="utf-8")
78
+
79
+ config = load_config(str(config_file))
80
+
81
+ assert config.timeout == 10
82
+ assert config.output_dir is None
83
+ assert config.base_url is None
84
+
85
+
86
+ def test_unknown_key_raises(tmp_path):
87
+ """An unrecognized key (e.g. a typo) is an error, not silently ignored."""
88
+ config_file = tmp_path / "typo.toml"
89
+ config_file.write_text("timout = 10\n", encoding="utf-8")
90
+
91
+ with pytest.raises(ConfigError):
92
+ load_config(str(config_file))
93
+
94
+
95
+ def test_wrong_value_type_raises(tmp_path):
96
+ """A value of the wrong type (string where int expected) is an error."""
97
+ config_file = tmp_path / "badtype.toml"
98
+ config_file.write_text('timeout = "thirty"\n', encoding="utf-8")
99
+
100
+ with pytest.raises(ConfigError):
101
+ load_config(str(config_file))
102
+
103
+
104
+ def test_non_utf8_file_raises_config_error(tmp_path):
105
+ """A config file that is not valid UTF-8 is a ConfigError, not a crash."""
106
+ config_file = tmp_path / "utf16.toml"
107
+ config_file.write_text("timeout = 30\n", encoding="utf-16")
108
+
109
+ with pytest.raises(ConfigError):
110
+ load_config(str(config_file))
111
+
112
+
113
+ def test_utf8_bom_file_is_tolerated(tmp_path):
114
+ """A UTF-8 file with a BOM (common on Windows) loads normally."""
115
+ config_file = tmp_path / "bom.toml"
116
+ config_file.write_text("timeout = 30\n", encoding="utf-8-sig")
117
+
118
+ config = load_config(str(config_file))
119
+
120
+ assert config.timeout == 30
@@ -71,3 +71,27 @@ def test_rendered_file_has_no_duplicate_functions():
71
71
  assert len(function_names) == len(set(function_names)), (
72
72
  "Rendered file contains duplicate function definitions"
73
73
  )
74
+
75
+
76
+ def test_build_request_call_without_timeout_unchanged():
77
+ """Without a timeout, the call contains no timeout argument."""
78
+ scenarios = _scenarios_for_tag("pet")
79
+ call = build_request_call(scenarios[0])
80
+ assert "timeout" not in call
81
+
82
+
83
+ def test_build_request_call_with_timeout():
84
+ """With a timeout, the call ends with a timeout keyword argument."""
85
+ scenarios = _scenarios_for_tag("pet")
86
+ call = build_request_call(scenarios[0], timeout=30)
87
+ assert "timeout=30" in call
88
+
89
+
90
+ def test_render_test_file_threads_timeout():
91
+ """A timeout given to render_test_file appears in every request call."""
92
+ scenarios = _scenarios_for_tag("pet")
93
+ rendered = render_test_file(
94
+ "test_pet.py", "http://localhost:8080", scenarios, timeout=15
95
+ )
96
+ compile(rendered, "test_pet.py", "exec")
97
+ assert rendered.count("timeout=15") == len(scenarios)
File without changes
File without changes
File without changes