Skip to content

Commit cad182c

Browse files
Merge pull request #211 from gibbonCode/ci-test-install
Add CI testing infrastructure
2 parents 024fad8 + dc3e3d8 commit cad182c

9 files changed

Lines changed: 424 additions & 1 deletion

File tree

.github/workflows/run_tests.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Run Tests
2+
3+
on:
4+
push:
5+
paths:
6+
- lib/**
7+
- tests/**
8+
- installGibbon.m
9+
- .github/workflows/run_tests.yml
10+
pull_request:
11+
paths:
12+
- lib/**
13+
- tests/**
14+
- installGibbon.m
15+
- .github/workflows/run_tests.yml
16+
17+
jobs:
18+
my-job:
19+
name: Run MATLAB Tests
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Check out repository
23+
uses: actions/checkout@v6
24+
with:
25+
fetch-depth: 1
26+
submodules: recursive
27+
28+
- name: Set up products
29+
uses: matlab-actions/setup-matlab@v3
30+
with:
31+
release: latest
32+
cache: true
33+
products: >
34+
Optimization_Toolbox
35+
Symbolic_Math_Toolbox
36+
Image_Processing_Toolbox
37+
Curve_Fitting_Toolbox
38+
39+
- name: Install GIBBON
40+
uses: matlab-actions/run-command@v3
41+
with:
42+
command: installGibbon
43+
44+
- name: Run tests
45+
uses: matlab-actions/run-tests@v3
46+
with:
47+
source-folder: lib
48+
select-by-folder: tests
49+
strict: false
50+
test-results-html: test-results
51+
code-coverage-cobertura: code-coverage/coverage.xml
52+
53+
- name: Upload coverage to Codecov
54+
uses: codecov/codecov-action@v4
55+
with:
56+
files: code-coverage/coverage.xml
57+
flags: matlab
58+
fail_ci_if_error: false

CONTRIBUTING.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@ gpublish HELP_functionName
2424
This will "publish" the documentation in the form of HTML files in the `GIBBON/docs/html` folder. The content there will then be automatically added to, and rendered on, the website when the website is updated.
2525

2626
## Testing
27-
For the moment the DEMO_ and HELP_ files may serves as a test suite. The `testGibbon` function can be used to run (and "publish" HTML if needed) these tests/demos automatically.
27+
28+
New code should define well-structured [unit tests](https://www.mathworks.com/help/matlab/matlab_prog/ways-to-write-unit-tests.html), stored in the `tests` directory and labeled `TEST_functionName.m`. These will
29+
be picked up by our CI pipeline and run on a remote GitHub runner, so make sure to keep your tests small and portable.
30+
31+
If you are new to unit tests, the examples `functionTestTemplate` and `ClassTestTemplate` might be a good starting point.
32+
33+
Unfortunately, the bulk of the code does not yet have structured tests. The `DEMO_*` and `HELP_*` files may serve as a more extensive test suite. The `testGibbon` function can be used to run (and "publish" HTML if needed) these tests/demos automatically.
2834

2935
## Pull requests
3036
- Try not to pollute your pull request with unintended changes – keep them simple and small. If possible, squash your commits.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
[![tests](https://github.com/gibbonCode/GIBBON/actions/workflows/run_tests.yml/badge.svg?event=push)](https://github.com/gibbonCode/GIBBON/actions/workflows/run_tests.yml)
2+
[![codecov](https://codecov.io/github/gibbonCode/GIBBON/graph/badge.svg?token=J0W2ZBWP3L)](https://codecov.io/github/gibbonCode/GIBBON)
13

24
<img src="docs/img/logos/gibbonLogoBanner_1018x300.png" href="https://gibboncode.org" alt="GIBBON" width="100%">
35

installGibbon.m

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ function installGibbon(interactive, FEBioPath, profileNameVCW)
110110
disp(opts);
111111
end
112112

113+
%% Check for Toolboxes
114+
requirements = {'Image Processing Toolbox', 'Statistics and Machine Learning Toolbox', 'Symbolic Math Toolbox', 'Curve Fitting Toolbox', 'Parallel Computing Toolbox'};
115+
fulfilled = ismember(requirements, {ver().Name});
116+
if ~all(fulfilled)
117+
warning('GIBBON:ToolboxCheck', 'Some required toolboxes might be missing:\n\t%s', strjoin(requirements(~fulfilled),'\n\t'));
118+
end
119+
113120
%% Unzip compressed data
114121
updateStatus('Unzipping compressed data');
115122

tests/ClassTestTemplate.m

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
classdef ClassTestTemplate < matlab.unittest.TestCase
2+
% Example of class-based unit tests. See:
3+
% https://www.mathworks.com/help/matlab/matlab_prog/class-based-unit-tests.html
4+
% https://www.mathworks.com/help/matlab/matlab_prog/ways-to-write-unit-tests.html
5+
%
6+
% If you're new to tests, you may want to start with function-based unit tests
7+
% See also: functionTestTemplate
8+
9+
%% (Optional) Test Parameters
10+
% TestParameter properties are used to define parameterized tests,
11+
% i.e. "variations" of a single test method, ran with different inputs.
12+
% See the 'testParameterizedMultiply' method below, and:
13+
% https://www.mathworks.com/help/matlab/matlab_prog/use-parameters-in-class-based-tests.html
14+
15+
properties (TestParameter)
16+
b = {3,4}
17+
a = struct('one', 1, 'two', 2) % use a struct to name parameter values
18+
end
19+
20+
%% (Optional) Custom Properties
21+
% Define your own properties to pass data between fixtures and test methods.
22+
23+
properties
24+
TempDir % used to store a temporary directory for the tests
25+
oldPath % used to store the current directory before each test
26+
end
27+
28+
%% (Optional) Fixtures
29+
% Fixtures run before and after tests, to do preparation or cleanup.
30+
31+
methods (TestClassSetup)
32+
function setupOnce(testCase)
33+
% Runs once for the whole class.
34+
% Use for expensive one-time setup (temp dirs, large test data, etc.).
35+
tempDir = tempname;
36+
mkdir(tempDir);
37+
testCase.TempDir = tempDir;
38+
end
39+
end
40+
41+
methods (TestClassTeardown)
42+
function teardownOnce(testCase)
43+
% Runs once after all tests in this class.
44+
if isfolder(testCase.TempDir)
45+
rmdir(testCase.TempDir, 's');
46+
end
47+
end
48+
end
49+
50+
methods (TestMethodSetup)
51+
function setup(testCase)
52+
% Runs before each test method.
53+
testCase.oldPath = pwd;
54+
cd(testCase.TempDir);
55+
end
56+
end
57+
58+
methods (TestMethodTeardown)
59+
function teardown(testCase)
60+
% Runs after each test method.
61+
cd(testCase.oldPath);
62+
end
63+
end
64+
65+
%% Test Methods
66+
67+
methods (Test)
68+
function testWritingFilesWorks(testCase)
69+
70+
writelines('Class tests are great!', 'test.txt');
71+
testCase.verifyTrue(isfile('test.txt'));
72+
end
73+
74+
function testParameterizedMultiply(testCase, a, b)
75+
% a, b will take the values defined in the TestParameter properties above
76+
77+
c = multiply(a,b);
78+
testCase.assertEqual(c, a*b);
79+
end
80+
end
81+
end
82+
83+
function c = multiply(a, b)
84+
c = a*b;
85+
end

tests/TEST_patchArea.m

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
classdef TEST_patchArea < matlab.unittest.TestCase
2+
properties (TestParameter)
3+
geom = testGeometries();
4+
end
5+
6+
methods (Test)
7+
function testPatchArea(testCase, geom)
8+
areaEstimate = sum(patchArea(geom.F, geom.V), 'all');
9+
testCase.verifyEqual(areaEstimate, geom.areaTotalTrue, 'RelTol', 3.5e-3);
10+
end
11+
end
12+
end
13+
14+
function C = testGeometries()
15+
16+
C = struct();
17+
for idx = 1:6
18+
switch idx
19+
case 1 % Circle
20+
name = 'circle';
21+
r = 1;
22+
n = 250;
23+
t = linspace(-pi, pi, n)';
24+
t = t(1:end-1);
25+
V = [r*cos(t) r*sin(t) zeros(size(t))];
26+
F = 1:size(V,1);
27+
areaTotalTrue = pi*r^2;
28+
case 2 % Single element square 1x1
29+
name = 'singleElementSquare';
30+
V = [0 0 0; 1 0 0; 0 1 0; 1 1 0];
31+
F = [1 2 4 3];
32+
areaTotalTrue = 1;
33+
case 3 % Multi-element square rotated 1x1
34+
name = 'multiElementSquareRotated';
35+
V1 = 0.5*[-1 -1; -1 1; 1 1; 1 -1];
36+
regionCell = {V1};
37+
plotOn = 0;
38+
pointSpacing = 0.1;
39+
resampleCurveOpt = 1;
40+
[F,V] = regionTriMesh2D(regionCell, pointSpacing, resampleCurveOpt, plotOn);
41+
V(:,3) = 0;
42+
R = euler2DCM([-0.25*pi 0.25*pi 0]);
43+
V = V*R;
44+
areaTotalTrue = 1;
45+
case 4 % Cube
46+
name = 'cube';
47+
[V,F] = platonic_solid(2);
48+
V = V./max(V(:))/2;
49+
areaTotalTrue = 6;
50+
case 5 % Sphere quads
51+
name = 'sphereQuads';
52+
r = 1;
53+
n = 4;
54+
[F,V] = quadSphere(n, r, 2);
55+
areaTotalTrue = 4*pi*r^2;
56+
case 6 % Sphere triangles
57+
name = 'sphereTriangles';
58+
r = 1;
59+
n = 4;
60+
[F,V] = geoSphere(n, r);
61+
areaTotalTrue = 4*pi*r^2;
62+
end
63+
64+
C.(name).F = F;
65+
C.(name).V = V;
66+
C.(name).areaTotalTrue = areaTotalTrue;
67+
end
68+
end

tests/TEST_patchVolume.m

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
classdef TEST_patchVolume < matlab.unittest.TestCase
2+
properties (TestParameter)
3+
geom = testGeometries();
4+
end
5+
methods (Test)
6+
function testPatchVolume(testCase, geom)
7+
makeAbsolute = 0;
8+
volEst=patchVolume(geom.F, geom.V, makeAbsolute);
9+
testCase.verifyEqual(volEst, geom.volTotalTrue, 'RelTol', 3.5e-3);
10+
end
11+
end
12+
end
13+
14+
function C = testGeometries()
15+
16+
C = struct();
17+
for idx = 1:9
18+
switch idx
19+
case 1
20+
name = 'trianglulatedSphere';
21+
r=2;
22+
ns=5;
23+
[F,V]=geoSphere(ns,r);
24+
volTotalTrue=4/3*pi*r^3; %True theoretical volume
25+
case 2 %Trianglulated sphere
26+
name = 'quadrangulatedSphere';
27+
r=2;
28+
ns=6;
29+
[F,V]=quadSphere(ns,r);
30+
volTotalTrue=4/3*pi*r^3; %True theoretical volume
31+
32+
%Shift randomly to create non-zero centred patch data
33+
V=V+10.*randn(1,3);
34+
case 3
35+
name = 'triangulatedTorus';
36+
r=1; %Sphere radius
37+
R=2; %Central radius
38+
nr=76;
39+
nc=150;
40+
[F,V]=patchTorus(r,nr,R,nc,'tri');
41+
volTotalTrue=2*pi^2*r*R; %True theoretical volume
42+
case 4
43+
name = 'quadrangulatedTorus';
44+
r=1; %Sphere radius
45+
R=2; %Central radius
46+
nr=76;
47+
nc=150;
48+
[F,V]=patchTorus(r,nr,R,nc,'quad');
49+
volTotalTrue=2*pi^2*r*R; %True theoretical volume
50+
case 5
51+
name = 'honeycombTorus';
52+
r=1; %Sphere radius
53+
R=2; %Central radius
54+
nr=76;
55+
nc=150;
56+
[F,V]=patchTorus(r,nr,R,nc,'honey');
57+
volTotalTrue=2*pi^2*r*R; %True theoretical volume
58+
case 6
59+
name = 'triangulatedBox';
60+
boxDim=[5 2 1];
61+
pointSpacing=1;
62+
[F,V]=triBox(boxDim,pointSpacing);
63+
volTotalTrue=prod(boxDim); %True theoretical volume
64+
case 7
65+
name = 'quadrangulatedBox';
66+
boxDim=[5 2 1];
67+
[F,V]=quadBox(boxDim,[5 2 1]);
68+
volTotalTrue=prod(boxDim); %True theoretical volume
69+
case 8
70+
name = 'mixedMeshOfPentahedraAndHexahedra';
71+
r=2;
72+
ns=5;
73+
[Ft,Vt]=geoSphere(ns,r);
74+
[V,F]=patch_dual(Vt,Ft);
75+
volTotalTrue=4/3*pi*r^3; %True theoretical volume
76+
case 9
77+
name = 'trianglulatedSphereWithFlippedFaces';
78+
r=2;
79+
ns=5;
80+
[F,V]=geoSphere(ns,r);
81+
F=fliplr(F);
82+
volTotalTrue=-4/3*pi*r^3; %True theoretical volume
83+
end
84+
C.(name).F = F;
85+
C.(name).V = V;
86+
C.(name).volTotalTrue = volTotalTrue;
87+
end
88+
end

0 commit comments

Comments
 (0)