Skip to content

Commit e768a68

Browse files
fix: 1st attempt to fix in an automated way
1 parent a8b169f commit e768a68

File tree

7 files changed

+372
-0
lines changed

7 files changed

+372
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Check if there are any custom extensions
2+
3+
from .junit_xml_fix import FixedJUnitXMLWriter
4+
5+
__all__ = [
6+
'FixedJUnitXMLWriter',
7+
]

terraform_compliance/extensions/ext_radish_bdd.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from radish import world
55
from terraform_compliance.common.defaults import Defaults
66
from terraform_compliance.common.error_handling import Error
7+
from .junit_xml_fix import FixedJUnitXMLWriter
78

89

910
def skip_step(step, resource=None, message=None):
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""
2+
Custom JUnit XML writer extension for terraform-compliance
3+
Fixes the UnboundLocalError when scenarios are skipped
4+
"""
5+
6+
import os
7+
import sys
8+
from datetime import datetime
9+
from xml.etree import ElementTree as ET
10+
from radish.extensions import CodeGenerator
11+
from radish.model import ScenarioLoop, ScenarioOutline
12+
from radish.hookregistry import after
13+
from radish import config
14+
15+
16+
class FixedJUnitXMLWriter(CodeGenerator):
17+
"""
18+
Fixed JUnit XML writer that properly handles skipped scenarios
19+
"""
20+
21+
OPTIONS = [
22+
(
23+
"--junit-xml=<PATH>",
24+
"write JUnit XML result file after run to <PATH>",
25+
)
26+
]
27+
28+
def __init__(self):
29+
# Disable the default radish JUnit XML writer
30+
self.junit_xml_path = None
31+
32+
def get_junit_xml_path(self):
33+
"""Get the JUnit XML output path from configuration"""
34+
return config().junit_xml
35+
36+
@after.each_feature
37+
def generate_junit_xml(self, feature):
38+
"""Generate JUnit XML report for the feature"""
39+
junit_xml_path = self.get_junit_xml_path()
40+
if not junit_xml_path:
41+
return
42+
43+
# Create testsuite element
44+
testsuite = ET.Element("testsuite")
45+
testsuite.set("name", feature.sentence)
46+
testsuite.set("tests", str(len(feature.scenarios)))
47+
testsuite.set("time", str(feature.duration))
48+
49+
# Count results
50+
failures = 0
51+
errors = 0
52+
skipped = 0
53+
54+
for scenario in feature.scenarios:
55+
if isinstance(scenario, (ScenarioLoop, ScenarioOutline)):
56+
scenarios = scenario.scenarios
57+
else:
58+
scenarios = [scenario]
59+
60+
for actual_scenario in scenarios:
61+
# Create testcase element for each scenario
62+
testcase = ET.SubElement(testsuite, "testcase")
63+
testcase.set("classname", feature.sentence)
64+
testcase.set("name", actual_scenario.sentence)
65+
testcase.set("time", str(actual_scenario.duration))
66+
67+
# Handle different scenario states
68+
if actual_scenario.state == "skipped":
69+
skipped += 1
70+
skipped_elem = ET.SubElement(testcase, "skipped")
71+
if hasattr(actual_scenario, 'skip_reason'):
72+
skipped_elem.set("message", str(actual_scenario.skip_reason))
73+
elif actual_scenario.state == "failed":
74+
failures += 1
75+
failure_elem = ET.SubElement(testcase, "failure")
76+
if hasattr(actual_scenario, 'failure_reason'):
77+
failure_elem.set("message", str(actual_scenario.failure_reason))
78+
if hasattr(actual_scenario, 'failure_traceback'):
79+
failure_elem.text = str(actual_scenario.failure_traceback)
80+
elif actual_scenario.state == "untested":
81+
errors += 1
82+
error_elem = ET.SubElement(testcase, "error")
83+
error_elem.set("message", "Scenario was not tested")
84+
85+
# Update testsuite attributes
86+
testsuite.set("failures", str(failures))
87+
testsuite.set("errors", str(errors))
88+
testsuite.set("skipped", str(skipped))
89+
90+
# Write XML file
91+
try:
92+
tree = ET.ElementTree(testsuite)
93+
# Create directory if it doesn't exist
94+
os.makedirs(os.path.dirname(junit_xml_path) or '.', exist_ok=True)
95+
tree.write(junit_xml_path, encoding="utf-8", xml_declaration=True)
96+
except Exception as e:
97+
print(f"Error writing JUnit XML: {e}", file=sys.stderr)
98+
99+
100+
# Register the extension
101+
def load_radish_extensions():
102+
"""Load the fixed JUnit XML writer extension"""
103+
return [FixedJUnitXMLWriter]

terraform_compliance/main.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ def cleanup():
4141
shutil.rmtree(Defaults().cache_dir)
4242

4343

44+
def setup_radish_extensions():
45+
"""Setup custom radish extensions including our JUnit XML fix"""
46+
from radish import config
47+
48+
# Import our custom extension
49+
from terraform_compliance.extensions.junit_xml_fix import FixedJUnitXMLWriter
50+
51+
# Register our extension
52+
return [FixedJUnitXMLWriter]
53+
54+
4455
def cli(arghandling=ArgHandling(), argparser=ArgumentParser(prog=__app_name__,
4556
description='BDD Test Framework for Hashicorp terraform')):
4657
atexit.register(cleanup)
@@ -152,6 +163,9 @@ def cli(arghandling=ArgHandling(), argparser=ArgumentParser(prog=__app_name__,
152163
console_write('\n{} Running tests. {}\n'.format(Defaults().icon,
153164
Defaults().tada))
154165

166+
# Before running radish, setup our extensions
167+
extensions = setup_radish_extensions()
168+
155169
try:
156170
result = call_radish(args=commands[1:])
157171
return result
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"format_version": "1.0",
3+
"terraform_version": "1.11.4",
4+
"planned_values": {
5+
"root_module": {
6+
"resources": [
7+
{
8+
"address": "aws_s3_bucket.example_code",
9+
"mode": "managed",
10+
"type": "aws_s3_bucket",
11+
"name": "example_code",
12+
"provider_name": "registry.terraform.io/hashicorp/aws",
13+
"values": {
14+
"bucket": "This is just a sample code"
15+
}
16+
}
17+
]
18+
}
19+
},
20+
"resource_changes": [
21+
{
22+
"address": "aws_s3_bucket.example_code",
23+
"mode": "managed",
24+
"type": "aws_s3_bucket",
25+
"name": "example_code",
26+
"provider_name": "registry.terraform.io/hashicorp/aws",
27+
"change": {
28+
"actions": ["create"]
29+
}
30+
}
31+
]
32+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
Unit tests for the fixed JUnit XML writer
3+
"""
4+
5+
import pytest
6+
import tempfile
7+
import os
8+
from unittest.mock import Mock, patch
9+
from xml.etree import ElementTree as ET
10+
11+
from terraform_compliance.extensions.junit_xml_fix import FixedJUnitXMLWriter
12+
13+
14+
class TestFixedJUnitXMLWriter:
15+
16+
def test_junit_xml_generation_with_skipped_scenario(self):
17+
"""Test that JUnit XML is generated correctly when scenarios are skipped"""
18+
with tempfile.TemporaryDirectory() as tmpdir:
19+
junit_path = os.path.join(tmpdir, "junit.xml")
20+
21+
# Create mock objects
22+
mock_scenario = Mock()
23+
mock_scenario.sentence = "Test scenario"
24+
mock_scenario.duration = 0.5
25+
mock_scenario.state = "skipped"
26+
mock_scenario.skip_reason = "Resource type not found"
27+
28+
mock_feature = Mock()
29+
mock_feature.sentence = "Test feature"
30+
mock_feature.duration = 1.0
31+
mock_feature.scenarios = [mock_scenario]
32+
33+
# Create writer and generate XML
34+
writer = FixedJUnitXMLWriter()
35+
36+
with patch('radish.config') as mock_config:
37+
mock_config.return_value.junit_xml = junit_path
38+
39+
writer.generate_junit_xml(mock_feature)
40+
41+
# Verify XML was created
42+
assert os.path.exists(junit_path)
43+
44+
# Parse and verify XML content
45+
tree = ET.parse(junit_path)
46+
root = tree.getroot()
47+
48+
assert root.tag == "testsuite"
49+
assert root.get("name") == "Test feature"
50+
assert root.get("tests") == "1"
51+
assert root.get("skipped") == "1"
52+
53+
# Check testcase
54+
testcases = root.findall("testcase")
55+
assert len(testcases) == 1
56+
57+
testcase = testcases[0]
58+
assert testcase.get("name") == "Test scenario"
59+
assert testcase.get("time") == "0.5"
60+
61+
# Check skipped element
62+
skipped = testcase.find("skipped")
63+
assert skipped is not None
64+
assert skipped.get("message") == "Resource type not found"
65+
66+
def test_junit_xml_generation_with_passed_scenario(self):
67+
"""Test that JUnit XML is generated correctly for passed scenarios"""
68+
with tempfile.TemporaryDirectory() as tmpdir:
69+
junit_path = os.path.join(tmpdir, "junit.xml")
70+
71+
# Create mock objects
72+
mock_scenario = Mock()
73+
mock_scenario.sentence = "Test scenario"
74+
mock_scenario.duration = 0.5
75+
mock_scenario.state = "passed"
76+
77+
mock_feature = Mock()
78+
mock_feature.sentence = "Test feature"
79+
mock_feature.duration = 1.0
80+
mock_feature.scenarios = [mock_scenario]
81+
82+
# Create writer and generate XML
83+
writer = FixedJUnitXMLWriter()
84+
85+
with patch('radish.config') as mock_config:
86+
mock_config.return_value.junit_xml = junit_path
87+
88+
writer.generate_junit_xml(mock_feature)
89+
90+
# Verify XML was created
91+
assert os.path.exists(junit_path)
92+
93+
# Parse and verify XML content
94+
tree = ET.parse(junit_path)
95+
root = tree.getroot()
96+
97+
assert root.tag == "testsuite"
98+
assert root.get("name") == "Test feature"
99+
assert root.get("tests") == "1"
100+
assert root.get("failures") == "0"
101+
assert root.get("errors") == "0"
102+
assert root.get("skipped") == "0"

0 commit comments

Comments
 (0)