Repository: abs-tudelft/vhdeps

Issue:
GHDL and vsim backends cannot handle multiple test cases in one file
See title. Individual test cases are represented as `VhdFile` objects everywhere in these backends, making use of the `VhdFile.unit` attribute, which can be `None` when multiple are defined per file. This may be fixable by representing the test cases as two-tuples of the library and entity name.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/abs-tudelft/vhdeps/pull/21?src=pr&el=h1) Report
> Merging [#21](https://codecov.io/gh/abs-tudelft/vhdeps/pull/21?src=pr&el=desc) into [master](https://codecov.io/gh/abs-tudelft/vhdeps/commit/85fb3cccacdfa99132170c6ba8ae58d300b3ebfd?src=pr&el=desc) will **decrease** coverage by `0.17%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/abs-tudelft/vhdeps/pull/21/graphs/tree.svg?width=650&token=vCiEieYED2&height=150&src=pr)](https://codecov.io/gh/abs-tudelft/vhdeps/pull/21?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #21      +/-   ##
==========================================
- Coverage     100%   99.82%   -0.18%     
==========================================
  Files           8        8              
  Lines         567      569       +2     
==========================================
+ Hits          567      568       +1     
- Misses          0        1       +1
```


| [Impacted Files](https://codecov.io/gh/abs-tudelft/vhdeps/pull/21?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [vhdeps/targets/ghdl.py](https://codecov.io/gh/abs-tudelft/vhdeps/pull/21/diff?src=pr&el=tree#diff-dmhkZXBzL3RhcmdldHMvZ2hkbC5weQ==) | `100% <ø> (ø)` | :arrow_up: |
| [vhdeps/targets/vsim.py](https://codecov.io/gh/abs-tudelft/vhdeps/pull/21/diff?src=pr&el=tree#diff-dmhkZXBzL3RhcmdldHMvdnNpbS5weQ==) | `100% <ø> (ø)` | :arrow_up: |
| [vhdeps/targets/shared.py](https://codecov.io/gh/abs-tudelft/vhdeps/pull/21/diff?src=pr&el=tree#diff-dmhkZXBzL3RhcmdldHMvc2hhcmVkLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [vhdeps/vhdl.py](https://codecov.io/gh/abs-tudelft/vhdeps/pull/21/diff?src=pr&el=tree#diff-dmhkZXBzL3ZoZGwucHk=) | `99.59% <0%> (-0.41%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/abs-tudelft/vhdeps/pull/21?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/abs-tudelft/vhdeps/pull/21?src=pr&el=footer). Last update [85fb3cc...dfc239d](https://codecov.io/gh/abs-tudelft/vhdeps/pull/21?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/vhdeps/targets/ghdl.py b/vhdeps/targets/ghdl.py
index a354670..3e0e2b8 100644
--- a/vhdeps/targets/ghdl.py
+++ b/vhdeps/targets/ghdl.py
@@ -140,7 +140,7 @@ def _run_test_case(output_file, test_case, vcd_dir, ghdl_elaborate, ghdl_run):
     exit_code, *_ = run_cmd(
         output_file,
         ghdl_elaborate,
-        '--work=%s' % test_case.lib,
+        '--work=%s' % test_case.file.lib,
         test_case.unit)
     if exit_code != 0:
         output_file.write('Elaboration for %s failed!\n' % test_case.unit)
@@ -151,13 +151,13 @@ def _run_test_case(output_file, test_case, vcd_dir, ghdl_elaborate, ghdl_run):
     vcd_switch = []
     if vcd_dir is not None:
         vcd_file = '%s/%s.%s.vcd' % (
-            vcd_dir, test_case.lib, test_case.unit)
+            vcd_dir, test_case.file.lib, test_case.unit)
         vcd_switch.append('--vcd=%s' % vcd_file)
     exit_code, stdout, *_ = run_cmd(
         output_file,
         ghdl_run,
-        '--work=' + test_case.lib, test_case.unit,
-        '--stop-time=' + test_case.get_timeout().replace(' ', ''),
+        '--work=' + test_case.file.lib, test_case.unit,
+        '--stop-time=' + test_case.file.get_timeout().replace(' ', ''),
         *vcd_switch)
     if 'simulation stopped by --stop-time' in stdout:
         code = 1
diff --git a/vhdeps/targets/shared.py b/vhdeps/targets/shared.py
index 6318ff2..7fd85b1 100644
--- a/vhdeps/targets/shared.py
+++ b/vhdeps/targets/shared.py
@@ -16,6 +16,7 @@
 
 import sys
 import fnmatch
+from collections import namedtuple
 
 def add_arguments_for_get_test_cases(parser):
     """Adds the appropriate command line arguments for the `get_test_cases()`
@@ -33,6 +34,8 @@ def add_arguments_for_get_test_cases(parser):
         'partial match. If no patterns are specified, the matcher defaults to '
         'a single \'*_tc\' pattern.')
 
+TestCase = namedtuple('TestCase', ('file', 'unit'))
+
 def get_test_cases(vhd_list, pattern=None, **_):
     """Filters the toplevel entities in `vhd_list` using the given pattern
     list, returning the resulting list."""
@@ -40,25 +43,21 @@ def get_test_cases(vhd_list, pattern=None, **_):
         pattern = ['*_tc']
     test_cases = []
     for top in vhd_list.top:
-        if top.unit is None:
-            raise NotImplementedError(
-                'vhdeps\' test case runners currently do not support having '
-                'multiple test cases per VHDL file.\nThe offending file is '
-                '"%s".' % top.fname)
-        include = False
-        for pat in pattern:
-            target = top.unit
-            if pat.startswith(':'):
-                target = top.fname
-                pat = pat[1:]
-            invert = False
-            if pat.startswith('!'):
-                invert = True
-                pat = pat[1:]
-            if fnmatch.fnmatchcase(target, pat):
-                include = not invert
-        if include:
-            test_cases.append(top)
+        for unit in top.entity_defs:
+            include = False
+            for pat in pattern:
+                target = unit
+                if pat.startswith(':'):
+                    target = top.fname
+                    pat = pat[1:]
+                invert = False
+                if pat.startswith('!'):
+                    invert = True
+                    pat = pat[1:]
+                if fnmatch.fnmatchcase(target, pat):
+                    include = not invert
+            if include:
+                test_cases.append(TestCase(top, unit))
     return test_cases
 
 def run_cmd(output_file, cmd, *args):
diff --git a/vhdeps/targets/vsim.py b/vhdeps/targets/vsim.py
index 0e10b0a..a2984e3 100644
--- a/vhdeps/targets/vsim.py
+++ b/vhdeps/targets/vsim.py
@@ -381,11 +381,11 @@ def _write_tcl(vhd_list, tcl_file, **kwargs):
     test_cases = get_test_cases(vhd_list, **kwargs)
     for test_case in test_cases:
         tcl_file.write('lappend testcases [list %s %s "%s"]\n' % (
-            test_case.lib, test_case.unit, test_case.get_timeout()))
+            test_case.file.lib, test_case.unit, test_case.file.get_timeout()))
     if len(test_cases) == 1:
         test_case = test_cases[0]
         tcl_file.write('simulate %s %s "%s"\n' % (
-            test_case.lib, test_case.unit, test_case.get_timeout()))
+            test_case.file.lib, test_case.unit, test_case.file.get_timeout()))
     else:
         tcl_file.write('regression\n')
 


Test patch:
diff --git a/tests/test_ghdl.py b/tests/test_ghdl.py
index f37a06d..a02b2a8 100644
--- a/tests/test_ghdl.py
+++ b/tests/test_ghdl.py
@@ -40,6 +40,16 @@ class TestGhdlSimple(TestCase):
         self.assertTrue('PASSED  test_tc' in out)
         self.assertTrue('Test suite PASSED' in out)
 
+    def test_multiple_per_file(self):
+        """Test that multiple test cases can exist in one file (GHDL)"""
+        code, out, _ = run_vhdeps('ghdl', '-i', DIR+'/complex/multi-tc-per-file')
+        self.assertEqual(code, 0)
+        self.assertTrue('working!' in out)
+        self.assertTrue('PASSED  foo_tc' in out)
+        self.assertTrue('PASSED  bar_tc' in out)
+        self.assertFalse('baz' in out)
+        self.assertTrue('Test suite PASSED' in out)
+
     def test_failure(self):
         """Test that a single failing test case results in failure (GHDL)"""
         code, out, _ = run_vhdeps('ghdl', '-i', DIR+'/simple/failure')
diff --git a/tests/test_patterns.py b/tests/test_patterns.py
index ebe47db..efa34b3 100644
--- a/tests/test_patterns.py
+++ b/tests/test_patterns.py
@@ -92,9 +92,14 @@ class TestPatterns(TestCase):
         self.assertFalse(bool(re.search(r'ghdl -r [^\n]*baz', out)))
 
     def test_multi_tc_per_file(self):
-        """Test multiple test cases per file (not supported)"""
+        """Test multiple test cases per file"""
         with local.env(PATH=DIR+'/ghdl/fake-ghdl:' + local.env['PATH']):
-            code, _, err = run_vhdeps('ghdl', '-i', DIR+'/complex/multi-tc-per-file')
-        self.assertEqual(code, 1)
-        self.assertTrue('NotImplementedError: vhdeps\' test case runners currently do '
-                        'not support having multiple test cases per VHDL file.' in err)
+            code, out, _ = run_vhdeps('ghdl', '-i', DIR+'/complex/multi-tc-per-file')
+        self.assertEqual(code, 0)
+        self.assertTrue(bool(re.search(r'ghdl -a [^\n]*test_tc.vhd', out)))
+        self.assertTrue(bool(re.search(r'ghdl -e [^\n]*foo_tc', out)))
+        self.assertTrue(bool(re.search(r'ghdl -e [^\n]*bar_tc', out)))
+        self.assertFalse(bool(re.search(r'ghdl -e [^\n]*baz', out)))
+        self.assertTrue(bool(re.search(r'ghdl -r [^\n]*foo_tc', out)))
+        self.assertTrue(bool(re.search(r'ghdl -r [^\n]*bar_tc', out)))
+        self.assertFalse(bool(re.search(r'ghdl -r [^\n]*baz', out)))
diff --git a/tests/test_vsim.py b/tests/test_vsim.py
index 2ab1e48..3228735 100644
--- a/tests/test_vsim.py
+++ b/tests/test_vsim.py
@@ -27,6 +27,12 @@ class TestVsimReal(TestCase):
         self.assertEqual(code, 0)
         self.assertTrue('working!' in out)
 
+    def test_multiple_per_file(self):
+        """Test running vsim on a file with multiple test cases"""
+        code, out, _ = run_vhdeps('ghdl', '-i', DIR+'/complex/multi-tc-per-file')
+        self.assertEqual(code, 0)
+        self.assertTrue('working!' in out)
+
     def test_failure(self):
         """Test running vsim on a single failing test case"""
         code, out, _ = run_vhdeps('vsim', '-i', DIR+'/simple/failure')