kennetek / gridfinity-rebuilt-openscad

A ground-up rebuild of the stock gridfinity bins in OpenSCAD
Other
1.27k stars 186 forks source link

scad unit tests #114

Open Ruudjhuu opened 1 year ago

Ruudjhuu commented 1 year ago

we have regression testing with https://github.com/kennetek/gridfinity-rebuilt-openscad/pull/76 but, we dont have unit testing possibilities. regression testing is good, when you already have a correct stl file and want to make sure, that the new build will also produce this stl file. Therefore regression testing can not be applied to new modeling. Thats where unit testing comes into play. I dont know how the whole model will look, but I know, that the z dimension will be 42. We need a way to make sure, that this assumption is true. Aka, the model really has a z dimension of 42.

Ruudjhuu commented 1 year ago

Test idea

Test scenario

Lets assume we want to test the folowing scad code (copied from code base):

module cut(x=0, y=0, w=1, h=1, t=1, s=1) {
    translate([0,0,-$dh-h_base])
    cut_move(x,y,w,h)
    block_cutter(clp(x,0,$gxx), clp(y,0,$gyy), clp(w,0,$gxx-x), clp(h,0,$gyy-y), t, s);
}

Test features

To unit-test the scad code block we need a few features in the test framework:

Proposal

Create seperate test .scad file for tests. Use comments to indicate how the test will look. The following snippet defines two testcases:

// Testcase: test_12345:3D
// Global: $gdh=2
// Global: $gxx=5
// Global: $gyy=10
// Mock: cut_move = cut_move_mock
// Mock: block_cutter = block_cutter_mock
// Assert: total_z = 22
cut(1,2,3,4,5);

// Testcase: test_54321:3D
// Global: $gdh=2
// Global: $gxx=5
// Global: $gyy=10
// Mock: cut_move = cut_move_mock
// Mock: block_cutter = block_cutter_mock
// Assert: total_z = 22
cut(5,4,3,2,1);

module cut_move_mock(a,b,c,f){translate[10,10,10]children();}
module block_cutter_mock(a,b,c,d,e,f){cube([10,10,10]);}

If the tested module has children it could look like:

// Testcase: test_children:3D
// Global: $gdh=2
// Global: $gxx=5
// Global: $gyy=10
// Mock: cut_move = cut_move_mock
// Mock: block_cutter = block_cutter_mock
// Assert: total_z = 22
cut(1,2,3,4,5){
  cube([1,1,1]);
  cube([2,2,2]);
}

module cut_move_mock(a,b,c,f){translate[10,10,10]children();}
module block_cutter_mock(a,b,c,d,e,f){cube([10,10,10]);}

Current implementation

This python snipit test exactly the same as the proposal.

class cut(TestCase):
    def setUp(self) -> None:
        self.module_test = ModuleTest(Module.from_file("cut", "gridfinity-rebuilt-utility.scad"), OutputType.STL)
        self.module_test.add_global_variable("gdh", 2)
        self.module_test.add_global_variable("gxx", 5)
        self.module_test.add_global_variable("gyy", 10)
        self.module_test.add_dependency(Module("cut_move", ["translate[10,10,10]children();";]))
        self.module_test.add_dependency(Module("block_cutter", ["cube([10,10,10]);"]))

    def tearDown(self) ->None:
        self.module_test.clean_up()

    def test_12345(self) -> None:
        self.module_test.add_arguments(1,2,3,4,5)
        self.module_test.run(self.id())
        self.assertEqual(self.module_test.svg_result.total_z, 22)

    def test_54321(self) -> None:
        self.module_test.add_arguments(5,4,3,2,1)
        self.module_test.run(self.id())
        self.assertEqual(self.module_test.svg_result.total_z, 22)

    # Testcase with children
    def test_children(self) -> None:
        self.module_test.add_arguments(5,4,3,2,1)
        self.module_test.add_children([ Cube([1,1,1]),
                                        Cube([2,2,2])])
        self.module_test.run(self.id())
        self.assertEqual(self.module_test.svg_result.total_z, 22)
DJDDDM commented 1 year ago

I think we arent fully on the same page, but we are getting definetly closer. Let me add some thoughts about your thoughts. My thoughts are in italic.

To unit-test the scad code block we need a few features in the test framework:

A test name. Not necessarily. All we really need to have is a possibility to know, which assertion failed. For this we could use the scad file name/path and line number. Either instead of the name, or as the default name. My Idea is to allow to write as few lines/characters as possible. Control the argument given to the module, also children if needed. Also not necessarily. Your approach is to import the whole module into python and then have it executed from there. My approach would be to have it run in scad/OpenSCAD and then just check the result. This could be done by using the show only modifier "!" written automatically into the scad code. if tests are writen in openscad, this would automaticaly be supported. are we able to create a shortcut in openscad to run all the tests? Predefine the global variables the global variables are defined by the openscad customizer profile. so tests can either just run using the current or a specifically named customizer profile Can be defined in scad file (hard to test different values) Add it as comment so the testframework can parse it, you can set different values for different testcases. Don't use global variables. It's bad. This is a different problem. This problem arises from the lag of nameable elements inside a bigger structure. We only have list/arrays and no classes, structs or dictionaries. Mock modules I highly doubt, that we will need Mocks. I also verry verry rarely use mocks in all the code I have written in my job. Can be possible by defining the mock in openscad. Add in a comment which function the definition should mock. Test framework should generate the mock. mock cut_move. mock block_cutter. assert outcome of the module. Test framework should support assert comments with easy to use assert posibilities. suport modules generating 3d and 2d output I think Openscad tells if the output is 2D or 3D but after the command has run, after you already need to know if it is a 2D or 3D output. IIRC all 2d objects are actually usable as 3d objects cause they have a certain height. At least in the preview_

// Testcase: test_12345:3D
// Global: $gdh=2
// Global: $gxx=5
// Global: $gyy=10
// Mock: cut_move = cut_move_mock
// Mock: block_cutter = block_cutter_mock
// Assert: total_z = 22
cut(1,2,3,4,5);

lets remove everything I think is not necessary. see above for the reasoning:

// Assert: total_z = 22
cut(1,2,3,4,5);

alternative Form:

cut(1,2,3,4,5); // Assert: total_z == 22

In my picture the first thing that happens is, that the test framework first applies the "!" operator to the code.

!cut(1,2,3,4,5); // Assert: total_z == 22

then it gets the updated preview from OpenSCAD.

the updated preview, together with the source line and its number is enough information to have the test run.

In my picture, all the end user developer has to do is to write a comment behind the call to the module and the rest is done by OpenSCAD and the test framework. The easier the tests the better it is. We can add more precision, if we have proven, that we need it.

Ruudjhuu commented 1 year ago

Lets first agree on the definition of a unit test:

My coments on your comments

Personal Conclusion

I do not see the added value to not write the tests in python. If so, we would add more complexity, another parser just to write the exact same tests in comments in a different txt file.
When using python we can also use modern language features when writing tests. Like linting, spell check, autocomplete, enz. If everything is defined in comments, you rely on the quality of the self written parser to check if a test is correctly written and only find out runtime.

kennetek commented 1 year ago

Small slightly-related tangent: The variables inside some of the files that use the $ symbol were from refactoring the code so that everything would be contained in modules (if I remember correctly). I did it in that slightly-bad way because I wanted it done quickly. I don't think that is the proper way to do things, those variables should probably be passed as arguments. Although, that may cause the argument count for many modules to increase dramatically. (Edit: I see that you already plan to remove them)