Adding test suite for widget rst parser.

Also splitting github actions into multiple workflows to make it
easier to see which test fails.
This commit is contained in:
Robert Tice
2020-11-19 16:57:26 -05:00
parent 437dcda306
commit e73dc9c8a5
15 changed files with 474 additions and 49 deletions

View File

@@ -1,4 +1,4 @@
name: Frontend Test Suite
name: Sphinx Content Tests
on: [push, pull_request]
@@ -6,9 +6,8 @@ defaults:
run:
working-directory: frontend
jobs:
test:
sphinx-content:
runs-on: ubuntu-18.04
@@ -30,22 +29,8 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Install OS Deps
run: |
sudo apt-get install -y \
graphviz \
texlive-latex-base \
texlive-latex-recommended \
texlive-latex-extra \
texlive-fonts-recommended \
texlive-fonts-extra \
latexmk \
texlive-xetex \
fonts-lmodern \
fonts-open-sans \
fonts-dejavu
- name: ada-actions/toolchain
uses: ada-actions/toolchain@v0.2.0
uses: ada-actions/toolchain@ce2020
with:
distrib: community
target: native
@@ -54,10 +39,6 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build HTML test pages
run: make site-testing
- name: Run Typescript tests
run: yarn run cover
- name: Run SPHINX engine tests
run: make SPHINXOPTS="-W" test_engine
- name: Run SPHINX content tests

View File

@@ -0,0 +1,35 @@
name: Sphinx Plugin Tests
on: [push, pull_request]
defaults:
run:
working-directory: frontend
jobs:
sphinx-plugin:
runs-on: ubuntu-18.04
strategy:
matrix:
python-version: [3.x]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: ada-actions/toolchain
uses: ada-actions/toolchain@ce2020
with:
distrib: community
target: native
community_year: 2020
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Test Sphinx Widget Parser Plugin
run: make test_parser

View File

@@ -0,0 +1,46 @@
name: Typescript Test Suite
on: [push, pull_request]
defaults:
run:
working-directory: frontend
jobs:
typescript-tests:
runs-on: ubuntu-18.04
strategy:
matrix:
python-version: [3.x]
node-version: [10.x]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: yarn --frozen-lockfile
- name: ada-actions/toolchain
uses: ada-actions/toolchain@ce2020
with:
distrib: community
target: native
community_year: 2020
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build HTML test pages
run: make site-testing
- name: Run Typescript tests
run: yarn run cover

3
frontend/.gitignore vendored
View File

@@ -5,4 +5,5 @@ build-manifest.json
dist
.nyc_output/
coverage/
yarn-error.log
yarn-error.log
.coverage

3
frontend/Makefile vendored
View File

@@ -71,6 +71,9 @@ test_content: test_ada_intro test_spark_intro test_advanced_ada test_advanced_sp
test_ada_for_the_cpp_java_developer \
test_gnat_toolchain_intro
test_parser:
@coverage run --source=widget -m unittest discover --start-directory sphinx
@coverage report -m
publish-staging:
@echo "Publishing current branch to learn-staging..."

View File

@@ -5,3 +5,4 @@ sphinx_rtd_theme
pydot-ng
pyparsing
graphviz
coverage

View File

View File

@@ -0,0 +1,15 @@
import unittest
from widget.button import Button, button_dictionary
class TestButton(unittest.TestCase):
def test_constructor(self):
button = Button("run_button")
self.assertEqual(button.name, button_dictionary["run_button"]["name"], msg="button name != dictionary lookup")
self.assertEqual(button.title, button_dictionary["run_button"]["title"], msg="button title != dictionary lookup")
self.assertEqual(button.mode, button_dictionary["run_button"]["mode"], msg="button mode != dictionary lookup")
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,132 @@
import unittest
from widget.chop import c_chop, cheapo_gnatchop, real_gnatchop
from widget.resource import Resource
class TestC_Chop(unittest.TestCase):
input = """Garbage
More garbage
!fake_file.txt
blahblah
nothing
!test.c
test.c
test.c
!test.h
test.h
test.h"""
def test_c_chop(self):
split = self.input.splitlines()
inTest = c_chop(split)
self.assertEqual(len(inTest), 2, msg="c chop returned more/less than 2 resources")
self.assertEqual(inTest[0].basename, "test.c", msg="basename parsing failed")
self.assertEqual(inTest[1].basename, "test.h", msg="basename parsing failed")
self.assertEqual(inTest[0].content, "\n".join(split[9:13]), msg=f"content parsing failed for {inTest[0].basename}")
self.assertEqual(inTest[1].content, "\n".join(split[14:]), msg=f"content parsing failed for {inTest[1].basename}")
class TestCheapo_Gnatchop(unittest.TestCase):
input = """Garbage
More garbage
!fake_file.txt
blahblah
nothing
package body A is
A Body
A Body
end A;
package A is
A Spec
A Spec
end A;
procedure Main is
main procedure
end Main;
"""
def test_cheapo_gnatchop(self):
split = self.input.splitlines()
inTest = cheapo_gnatchop(split)
self.assertEqual(len(inTest), 3, msg="cheapo chop returned more/less than 3 resources")
self.assertEqual(inTest[0].basename, "a.adb", msg="basename parsing failed")
self.assertEqual(inTest[1].basename, "a.ads", msg="basename parsing failed")
self.assertEqual(inTest[2].basename, "main.adb", msg="basename parsing failed")
self.assertEqual(inTest[0].content, "\n".join(split[8:14]), msg=f"content parsing failed for {inTest[0].basename}")
self.assertEqual(inTest[1].content, "\n".join(split[14:20]), msg=f"content parsing failed for {inTest[1].basename}")
self.assertEqual(inTest[2].content, "\n".join(split[20:]), msg=f"content parsing failed for {inTest[2].basename}")
class TestReal_Gnatchop(unittest.TestCase):
good_input = """package body A is
end A;
package A is
end A;
procedure Main is
begin
null;
end Main;
"""
bad_input = """Garbage
More garbage
!fake_file.txt
blahblah
nothing
package body A is
A Body
A Body
end A;
package A is
A Spec
A Spec
end A;
procedure Main is
main procedure
end Main;
"""
def test_real_gnatchop(self):
split = self.good_input.splitlines()
inTest = real_gnatchop(split)
self.assertEqual(len(inTest), 3, msg="real chop returned more/less than 3 resources")
self.assertEqual(inTest[0].basename, "a.adb", msg="basename parsing failed")
self.assertEqual(inTest[1].basename, "a.ads", msg="basename parsing failed")
self.assertEqual(inTest[2].basename, "main.adb", msg="basename parsing failed")
self.assertEqual(inTest[0].content, "\n".join(split[0:3]), msg=f"content parsing failed for {inTest[0].basename}")
self.assertEqual(inTest[1].content, "\n".join(split[4:7]), msg=f"content parsing failed for {inTest[1].basename}")
self.assertEqual(inTest[2].content, "\n".join(split[8:]), msg=f"content parsing failed for {inTest[2].basename}")
def test_real_gnatchop_error(self):
split = self.bad_input.splitlines()
with self.assertRaises(Exception):
real_gnatchop(split)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,22 @@
import unittest
from widget.resource import Resource
class TestResource(unittest.TestCase):
def test_constructor(self):
resourceA = Resource("ResourceA", ["A", "B"])
resourceB = Resource("ResourceB")
self.assertEqual(resourceA.basename, "ResourceA", msg="basename not stored properly")
self.assertEqual(resourceB.basename, "ResourceB", msg="basename not stored properly")
self.assertEqual(resourceA.content, "A\nB", msg="content not parsed correctly")
self.assertEqual(resourceB.content, "", msg="content not parsed correctly")
def test_append(self):
resourceC = Resource("ResourceC", ["A", "B"])
resourceC.append("C")
self.assertEqual(resourceC.content, "A\nB\nC", msg="string not appended properly")
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,178 @@
import unittest
from widget.widget import Widget, NameException, ButtonException, ChopException, LabException, FileException
class TestWidget(unittest.TestCase):
def setUp(self):
self.widget = Widget()
def test_parseArgs_name(self):
name = "test"
self.widget.parseArgs(["project=" + name])
self.assertEqual(self.widget.name, name)
self.assertFalse(self.widget.is_lab)
def test_parseArgs_lab(self):
name = "test"
self.widget.parseArgs(["lab=" + name])
self.assertEqual(self.widget.name, name)
self.assertTrue(self.widget.is_lab)
self.assertEqual(len(self.widget.button_group), 1)
self.assertEqual(self.widget.button_group[0].mode, "submit")
def test_parseArgs_noname(self):
with self.assertRaises(NameException):
self.widget.name
def test_parseArgs_button(self):
self.widget.parseArgs(["run_button"])
self.assertEqual(len(self.widget.button_group), 1)
self.assertEqual(self.widget.button_group[0].mode, "run")
def test_parseArgs_nobutton(self):
self.widget.parseArgs(["no_button", "run_button"])
self.assertEqual(len(self.widget.button_group), 0)
def test_parseArgs_missingbutton(self):
with self.assertRaises(ButtonException):
self.widget.button_group
def test_parseArgs_switches(self):
switches_in = "A(a,b,c);B(d,e,f);A(g)"
switches_out = {
"A": ["a", "b", "c", "g"],
"B": ["d", "e", "f"]
}
self.widget.parseArgs(["switches=" + switches_in])
self.assertDictEqual(self.widget.switches, switches_out)
def test_parseArgs_invalid(self):
with self.assertRaises(ValueError):
self.widget.parseArgs(['invalid'])
def test_parseOpts_nocheck(self):
self.widget.parseArgs(["run_button"])
self.assertEqual(len(self.widget.button_group), 1)
self.widget.parseOpts({"class": ["ada-nocheck"]})
self.assertEqual(len(self.widget.button_group), 0)
def test_parseOpts_syntaxonly(self):
self.widget.parseArgs(["run_button"])
self.assertEqual(len(self.widget.button_group), 1)
self.widget.parseOpts({"class": ["ada-syntax-only"]})
self.assertEqual(len(self.widget.button_group), 0)
def test_parseOpts_invalid(self):
with self.assertRaises(ValueError):
self.widget.parseOpts({"class": ["invalid"]})
def test_nofiles(self):
with self.assertRaises(FileException):
self.widget.files
def test_parseContent_adasimple(self):
ada = """package body A is
end A;
package A is
end A;
procedure Main is
begin
null;
end Main;
"""
self.widget.parseArgs(["project=Test", "ada"])
self.widget.parseContent(ada.splitlines())
self.assertEqual(len(self.widget.files), 3)
def test_parseContent_csimple(self):
c = """Garbage
More garbage
!fake_file.txt
blahblah
nothing
!test.c
test.c
test.c
!test.h
test.h
test.h"""
self.widget.parseArgs(["project=Test", "c"])
self.widget.parseContent(c.splitlines())
self.assertEqual(len(self.widget.files), 2)
def test_parseContent_lab(self):
lab = """
-- START LAB IO BLOCK
lab_io
-- END LAB IO BLOCK
package body A is
end A;
package A is
end A;
procedure Main is
begin
null;
end Main;
"""
split = lab.splitlines()
self.widget.parseArgs(["ada", "lab=Test"])
self.widget.parseContent(split)
self.assertEqual(len(self.widget.shadow_files), 1)
self.assertEqual(self.widget.shadow_files[0].basename, "lab_io.txt")
self.assertEqual(self.widget.shadow_files[0].content, split[2])
self.assertEqual(len(self.widget.files), 3)
def test_parseContent_lab_dup_startblock(self):
lab = """
-- START LAB IO BLOCK
lab_io
-- START LAB IO BLOCK
-- END LAB IO BLOCK
"""
self.widget.parseArgs(["manual_chop", "lab=Test"])
with self.assertRaises(LabException):
self.widget.parseContent(lab.splitlines())
def test_parseContent_lab_endfirst(self):
lab = """
-- END LAB IO BLOCK
-- START LAB IO BLOCK
"""
self.widget.parseArgs(["manual_chop", "lab=Test"])
with self.assertRaises(LabException):
self.widget.parseContent(lab.splitlines())
def test_parseContent_lab_no_end(self):
lab = """
-- START LAB IO BLOCK
"""
self.widget.parseArgs(["manual_chop", "lab=Test"])
with self.assertRaises(LabException):
self.widget.parseContent(lab.splitlines())
def test_parseContent_nochop(self):
self.widget.parseArgs(["project=Test"])
with self.assertRaises(ChopException):
self.widget.parseContent([])
if __name__ == '__main__':
unittest.main()

View File

@@ -42,7 +42,8 @@ def c_chop(lines: List[str]) -> List[Resource]:
match = re_fn.match(j)
if match:
# we found a new file
results.append(Resource(match.group(1)))
r = Resource(match.group(1))
results.append(r)
elif results:
# we append this line to the last file we found
results[-1].append(j)
@@ -76,10 +77,12 @@ def cheapo_gnatchop(lines: List[str]) -> List[Resource]:
# we found a new body file
newBasename = to_base_filename(bodyMatch.group(2)) + ".adb"
results.append(Resource(newBasename))
results[-1].append(j)
elif specMatch:
# we found a new spec file
newBasename = to_base_filename(specMatch.group(2)) + ".ads"
results.append(Resource(newBasename))
results[-1].append(j)
elif results:
# we append this line to the last file we found
results[-1].append(j)

View File

@@ -6,7 +6,7 @@ class Resource:
Resource represents a file object and has a filename (basename) and
contents.
"""
def __init__(self, basename: str, content: List[str] = []):
def __init__(self, basename: str, content: List[str] = None):
"""Constructs a Resource
Args:
@@ -14,7 +14,10 @@ class Resource:
content (List[str], optional): Initial contents. Defaults to [].
"""
self.basename = basename
self.__content = content
if content:
self.__content = content
else:
self.__content = []
def append(self, content: str):
"""Appends a string to the Resource's contents

View File

@@ -1,5 +1,5 @@
{% autoescape true %}
<widget id="{{ w.id }}" data-url="{{ url }}" data-name="{{ w.name }}" data-lab="{{ w.is_lab }}" data-switches='{{ w.switches | tojson }}' data-include='{{ w.include | tojson }}' data-hidden="{{ w.hidden }}">
<widget id="{{ w.id }}" data-url="{{ url }}" data-name="{{ w.name }}" data-lab="{{ w.is_lab }}" data-switches='{{ w.switches | tojson }}'>
{% for sf in w.shadow_files %}
<div class="shadow-file" style="display:none" data-basename="{{ sf.basename }}">{{ sf.content }}</div>
{% endfor %}
@@ -42,9 +42,6 @@
{% endfor %}
</div>
<div class="col-md-9 output-group">
{% if w.cli_input %}
<textarea id="{{ w.id }}.cli" class="cli" name="cli" rows="4" cols="50"></textarea>
{% endif %}
<div id="{{ w.id }}.output-area" class="output-area"></div>
{% if w.is_lab %}
<div id="{{ w.id }}.lab-area" class="lab-area"></div>

View File

@@ -6,6 +6,25 @@ from .button import Button
from .chop import c_chop, cheapo_gnatchop, real_gnatchop, ChopStrategy
from .resource import Resource
class ButtonException(Exception):
pass
class NameException(Exception):
pass
class ChopException(Exception):
pass
class LabException(Exception):
pass
class FileException(Exception):
pass
class Widget:
"""The Widget class defines the layout of a widget
@@ -23,7 +42,6 @@ class Widget:
"""
self.shadow_files: List[Resource] = []
self.is_lab: bool = False
self.cli_input = False
self.id = next(self.__count)
self.switches: Dict[str, List[str]] = {}
@@ -32,8 +50,6 @@ class Widget:
self.__name: str = None
self.__no_button: bool = False
self.__chop_strategy: ChopStrategy = None
self.hidden: bool = False
self.include: List[str] = []
@property
def button_group(self) -> List[Button]:
@@ -49,7 +65,7 @@ class Widget:
return []
else:
if not self.__button_group:
raise Exception('No buttons specified on widget')
raise ButtonException('No buttons specified on widget')
return self.__button_group
@property
@@ -65,7 +81,7 @@ class Widget:
if self.__name:
return self.__name
else:
raise Exception('No name specified on widget')
raise NameException('No name specified on widget')
@property
def files(self) -> List[Resource]:
@@ -80,7 +96,7 @@ class Widget:
if self.__files:
return self.__files
else:
raise Exception('No files present on widget')
raise FileException('No files present on widget')
def __parseButton(self, btn: str):
"""Parse a button specified in a Directive arg
@@ -158,7 +174,7 @@ class Widget:
# we found a start block
if lab_resource:
# this must be a duplicate start block
raise Exception('Duplicate start block found in Lab io')
raise LabException('Duplicate start block found in Lab io')
else:
lab_resource = Resource(labio_filename)
elif end_match:
@@ -170,7 +186,7 @@ class Widget:
return result
else:
# this must be an end before a start
raise Exception('End block found before start in Lab io')
raise LabException('End block found before start in Lab io')
elif lab_resource:
# this must be lab io data
lab_resource.append(j)
@@ -180,11 +196,7 @@ class Widget:
i += 1
# if we reach here, we never found an end block
raise Exception('No end block found before start in Lab IO')
def __parseInclude(self, arg: str):
includes = arg.split('=')[1]
self.include.extend(includes.split(','))
raise LabException('No end block found before start in Lab IO')
def parseArgs(self, args: List[str]):
"""Parses Directive arguments
@@ -213,12 +225,6 @@ class Widget:
self.__chop_strategy = ChopStrategy.REAL
elif arg.startswith('switches='):
self.__parseSwitches(arg)
elif arg == 'cli_input':
self.cli_input = True
elif arg == 'hidden':
self.hidden = True
elif arg.startswith('include='):
self.__parseInclude(arg)
else:
raise ValueError(f'Invalid argument: {arg}')
@@ -264,3 +270,5 @@ class Widget:
self.__files = cheapo_gnatchop(content)
elif self.__chop_strategy is ChopStrategy.REAL:
self.__files = real_gnatchop(content)
else:
raise ChopException('No chop strategy defined')