Skip to content

Commit

Permalink
test to become 1 commit
Browse files Browse the repository at this point in the history
Detecting duplicated components

Add check to detect duplicated components of a record
when parsing a TRLC file.

Example:
```
Something duplicated
{
	description = "This is fine!"
	description = "This is the duplicate."
}
```

The behavior of the `Record_Object` class is changed such that the `assign` method only
assigns components to the record if it has not been assigned already.
This is implemented by checking if the field type is different from `Implicit_Null`.

The `Parser` class asks the `Record_Object` if the component is `Implicit_Null`.
If no, then an error is sent to the message handler.
  • Loading branch information
phiwuu authored and Diego Fernandez committed Dec 17, 2024
1 parent 629f9ec commit d8f23f7
Show file tree
Hide file tree
Showing 15 changed files with 273 additions and 6 deletions.
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
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: docs test style lint package
.PHONY: docs test style lint package squash-commits

lint: style
@python3 -m pylint --rcfile=pylint3.cfg \
Expand Down Expand Up @@ -111,3 +111,12 @@ 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."

squash-commits:
util/squash_commits.sh
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
9 changes: 7 additions & 2 deletions language-reference-manual/lrm.trlc
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package LRM

Versioning Version {
major = 3
minor = 0
major = 7
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
63 changes: 63 additions & 0 deletions util/squash_commits.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/bin/bash

set -e # Exit immediately if any command exits with a non-zero status

# Function: check if the environment is clean
# checks for unstagged (diff --quiet) or stagged but uncommited changes (git diff --cached --quiet)
check_clean_worktree() {
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Error: Uncommitted changes detected. Please commit or stash changes first."
exit 1
fi
}

fetch_and_rebase() {
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
git fetch origin
echo "Checking if the branch is up to date with 'origin/$CURRENT_BRANCH'..."
if ! git diff --quiet "$CURRENT_BRANCH" "origin/$CURRENT_BRANCH"; then
echo "Branch is out of date. Rebasing onto 'origin/$CURRENT_BRANCH'..."
git rebase "origin/$CURRENT_BRANCH" || handle_rebase_conflicts
fi
}

handle_rebase_conflicts() {
while ! git rebase --continue 2>/dev/null; do
echo "Resolve conflicts, stage the changes, and reattempting 'git rebase --continue'..."
read -p "Press ENTER after resolving conflicts and staging changes."
done
}

# Squash commits interactively
interactive_squash() {
echo "Starting interactive rebase to squash commits into one..."
git rebase -i origin/main || handle_rebase_conflicts
}

# Function to force push changes after confirmation
force_push_with_confirmation() {
read -p "Do you want to force push the changes to the remote branch? (yes/no): " RESPONSE
case "$RESPONSE" in
[Yy][Ee][Ss]|[Yy])
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "Force pushing changes to 'origin/$CURRENT_BRANCH'..."
git push --force-with-lease
echo "Changes successfully pushed to remote."
;;
*)
echo "Force push canceled. Your changes are local only."
;;
esac
}

# Main script execution
main() {
check_clean_worktree
fetch_and_rebase
interactive_squash
echo "Rebase and squash completed successfully."
force_push_with_confirmation
}

# Entry point
main

0 comments on commit d8f23f7

Please sign in to comment.