Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detecting duplicated components #121

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ docs
*.pyc
*.egg-info
.venv/
venv/
build
dist
launch.json
settings.json

# EMACS ignores
*~
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ generated in the following situations:
* [TRLC] Fix an UnboundLocalError when missing a term
in an expression.

* Adds the feature of checking for duplicated records
in `*.trlc` and `*.rsl` files. This led to mistakenly
overwrite already set attribute's value.

### 2.0.0

This new major release includes a number of incompatible
Expand Down
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,9 @@ tracing: report.lobster
mkdir -p docs
lobster-html-report report.lobster --out=docs/tracing.html
lobster-ci-report report.lobster

clean-coverage:
@rm -rf htmlcov
@find . -name '.coverage*' -type f -delete
@find . -name '*.pyc' -type f -delete
@echo "All .coverage, .coverage.* and *.pyc files deleted."
3 changes: 2 additions & 1 deletion documentation/LRM.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# TRLC LRM

* [Version 3.0](https://bmw-software-engineering.github.io/trlc/lrm-3.0.html) (Current Stable)
* [Version 3.1](https://bmw-software-engineering.github.io/trlc/lrm.html) (Current Stable)
* [Version 3.0](https://bmw-software-engineering.github.io/trlc/lrm-3.0.html)
* [Version 2.9](https://bmw-software-engineering.github.io/trlc/lrm-2.9.html)
* [Version 2.8](https://bmw-software-engineering.github.io/trlc/lrm-2.8.html)
* [Version 2.7](https://bmw-software-engineering.github.io/trlc/lrm-2.7.html)
Expand Down
7 changes: 6 additions & 1 deletion language-reference-manual/lrm.trlc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package LRM

Versioning Version {
major = 3
minor = 0
minor = 1
}

GFDL_License License {
Expand Down Expand Up @@ -1775,6 +1775,11 @@ section "Record object declarations" {
references to R.)*'''
}

Dynamic_Semantics Single_Value_Assignment {
text = '''A value can be assigned to a component of a record object
only once.'''
}

Note Tuple_Checks {
text = '''The checks for a tuple aggregate are immediately evaluated
after the last value is parsed.'''
Expand Down
6 changes: 6 additions & 0 deletions tests-system/rbt-single-value-assignment/foo.rsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package Example

type Requirement {
description String
critical Boolean
}
23 changes: 23 additions & 0 deletions tests-system/rbt-single-value-assignment/foo.trlc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package Example

Requirement shape {
description = "Our car shall have four wheels."
critical = false
}

Requirement safety {
critical = false
description = "Our car shall not explode."
critical = true
}

Requirement form {
description = "Our car should look nice."
critical = false
}

Requirement duplicated {
description = "This is fine"
critical = true
description = "This is not fine anymore"
}
5 changes: 5 additions & 0 deletions tests-system/rbt-single-value-assignment/output
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
critical = true
^^^^^^^^ rbt-single-value-assignment/foo.trlc:11: error: component 'critical' already assigned at line 9
description = "This is not fine anymore"
^^^^^^^^^^^ rbt-single-value-assignment/foo.trlc:22: error: component 'description' already assigned at line 20
Processed 1 model and 1 requirement file and found 2 errors
2 changes: 2 additions & 0 deletions tests-system/rbt-single-value-assignment/output.brief
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rbt-single-value-assignment/foo.trlc:11:3: trlc error: component 'critical' already assigned at line 9
rbt-single-value-assignment/foo.trlc:22:3: trlc error: component 'description' already assigned at line 20
5 changes: 5 additions & 0 deletions tests-system/rbt-single-value-assignment/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
critical = true
^^^^^^^^ rbt-single-value-assignment/foo.trlc:11: error: component 'critical' already assigned at line 9
description = "This is not fine anymore"
^^^^^^^^^^^ rbt-single-value-assignment/foo.trlc:22: error: component 'description' already assigned at line 20
Processed 1 model and 1 requirement file and found 2 errors
5 changes: 5 additions & 0 deletions tests-system/rbt-single-value-assignment/output.smtlib
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
critical = true
^^^^^^^^ rbt-single-value-assignment/foo.trlc:11: error: component 'critical' already assigned at line 9
description = "This is not fine anymore"
^^^^^^^^^^^ rbt-single-value-assignment/foo.trlc:22: error: component 'description' already assigned at line 20
Processed 1 model and 1 requirement file and found 2 errors
128 changes: 126 additions & 2 deletions tests-unit/test_ast_bysection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import unittest
from unittest.mock import patch, MagicMock
from trlc.ast import Symbol_Table
from unittest.mock import MagicMock, patch
from trlc.ast import (Record_Object, Implicit_Null, Literal, Record_Type,
Package, Composite_Type, Composite_Component, Symbol_Table)
from trlc.errors import Location

class TestRecordObject:
def __init__(self, location, section):
Expand Down Expand Up @@ -38,3 +40,125 @@ def test_iter_record_objects_by_section(self, mock_iter_record_objects):

if __name__ == '__main__':
unittest.main()

"""
class TestCompositeType(Composite_Type):
def __init__(self):
super().__init__(name="mock_composite_type",
location=Location(file_name="mock_file", line_no=1, col_no=1),
description="mock description")
self.components = {}


class TestCompositeComponent(Composite_Component):
def __init__(self, name):
member_of = TestCompositeType()
super().__init__(name=name,
description="test_description",
location=Location(file_name="mock_file", line_no=1, col_no=1),
member_of=member_of,
n_typ=None,
optional=False)


class TestLiteral(Literal):
def __init__(self):
location = Location(file_name="test_file", line_no=1, col_no=1)
super().__init__(location=location, typ=None)

def can_be_null(self):
return False

def dump(self, indent=0):
pass

def to_python_object(self):
return "test_literal_value"

def to_string(self):
return "test_literal"


class TestRecordObjectMethods(unittest.TestCase):
def setUp(self):
name = "mock_name"
location = Location(file_name="mock_file", line_no=1, col_no=1)
builtin_stab = Symbol_Table()
declared_late = False
package = Package(
name="mock_package",
location=location,
builtin_stab=builtin_stab,
declared_late=declared_late,
)
description = "mock_description"
n_parent = None
is_abstract = False
n_typ = Record_Type(
name=name,
description=description,
location=location,
package=package,
n_parent=n_parent,
is_abstract=is_abstract,
)
section = None
n_package = package
self.record = Record_Object(
name=name,
location=location,
n_typ=n_typ,
section=section,
n_package=n_package,
)
self.record.field = {}

def test_is_component_implicit_null_uninitialized(self):
# Intention is to check if it correctly identifies an uninitialized component like a
# component with an Implicit_Null value in the field dictionary.

component = TestCompositeComponent("test_component")
self.record.field[component.name] = Implicit_Null(self.record, component)
self.assertTrue(self.record.is_component_implicit_null(component))

def test_is_component_implicit_null_initialized(self):
# Intention is to verify that it returns False for initialized components
# like when a valid value has been assigned already.
component = TestCompositeComponent("test_component")
self.record.field[component.name] = TestLiteral()
self.assertFalse(self.record.is_component_implicit_null(component))

def test_assign_uninitialized_component(self):
# Intention is to test whether the assign method correctly assigns a value
# to a component that is currently uninitialized (set to Implicit_Null).

component = TestCompositeComponent("new_component")
value = TestLiteral()
self.record.field[component.name] = Implicit_Null(self.record, component)
self.record.assign(component, value)
self.assertEqual(self.record.field[component.name], value)

def test_assign_duplicate_component(self):
# Intention is to confirm that the assign method raises an error
# when attempting to assign a value to a component that has already been assigned a value.
component = TestCompositeComponent("duplicate_component")
initial_value = TestLiteral()
self.record.field[component.name] = initial_value
new_value = TestLiteral()
with self.assertRaises(KeyError):
self.record.assign(component, new_value)

if __name__ == '__main__':
unittest.main()



The testing by using mocking requires instantiating many classes of the AST the because of how the assertions at runtime check and verify certain conditions about the arguments. If those conditions aren't satisfied perfectly, the code fails immediately, even if the behavior being tested is unrelated to the failed assertion.

Assertions in methods like `assign` in `ast.py` (arround line 3000) check for specific types and states of dependencies (e.g., ensuring arguments are instances of Composite_Component, Record_Object, or Implicit_Null). This means one must carefully construct the exact objects expected by the assertions everytime, which involves setting up a chain of related objects and probbably mocking again even more dependencies. The `assert` statements enforce all dependencies perfectly to be configured.

In order to achieve this 'perfectness' within the assertions some test classes like TestLiteral, TestCompositeComponent, and TestCompositeType were created purely as 'dummy' classes, allowing to bypass the complex setups required for the actual classes (Literal, Composite_Component, Composite_Type).

Propposed solution for testing: Adopt System-Level Testing
- Instead of testing individual methods in isolation, test the system's behavior as a whole using realistic input files or configurations and comparing against a expected output of the system given specific inputs, rather than low-level internal states.
"""
6 changes: 6 additions & 0 deletions trlc/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -2999,6 +2999,9 @@ def to_python_dict(self):
return {name: value.to_python_object()
for name, value in self.field.items()}

def is_component_implicit_null(self, component) -> bool:
return not isinstance(self.field[component.name], Implicit_Null)

def assign(self, component, value):
assert isinstance(component, Composite_Component)
assert isinstance(value, (Literal,
Expand All @@ -3008,6 +3011,9 @@ def assign(self, component, value):
Implicit_Null,
Unary_Expression)), \
"value is %s" % value.__class__.__name__
if self.is_component_implicit_null(component):
raise KeyError(f"Component {component.name} already \
assigned to {self.n_typ.name} {self.name}!")
self.field[component.name] = value

def dump(self, indent=0): # pragma: no cover
Expand Down
6 changes: 6 additions & 0 deletions trlc/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,7 @@ def parse_record_object_declaration(self):
# lobster-trace: LRM.Valid_Enumeration_Literals
# lobster-trace: LRM.Mandatory_Components
# lobster-trace: LRM.Evaluation_Of_Checks
# lobster-trace: LRM.Single_Value_Assignment

r_typ = self.parse_qualified_name(self.default_scope,
ast.Record_Type)
Expand All @@ -1810,6 +1811,11 @@ def parse_record_object_declaration(self):
comp = r_typ.components.lookup(self.mh,
self.ct,
ast.Composite_Component)
if obj.is_component_implicit_null(comp):
self.mh.error(self.ct.location,
"component '%s' already assigned at line %i" %
(comp.name,
obj.field[comp.name].location.line_no))
comp.set_ast_link(self.ct)
if r_typ.is_frozen(comp):
self.mh.error(self.ct.location,
Expand Down
Loading