├── README.md ├── cartesian.js ├── example.js └── matrix.png /README.md: -------------------------------------------------------------------------------- 1 | # Cartesian - generator for complex configurations 2 | 3 | There's a specific subset of problems in IT industry that have to do with big and somewhat regular domains, but not fully so. 4 | 5 | Consider the task of configuring a test suite. You have a fleet of different boxes, with different processors, different operating systems, different compilers, you want to run different tests and to do so with different compile-time and run-time options. 6 | 7 | In theory, the test suite would be a perfectly regular N-dimensional matrix featuring all the dimensions mentioned above. But that's where the complexity kicks in: Oh! MSVC only works on Windows! Test X requires 8G of memory and the box Y only has 4G available. Shared libraries have .so extension on Linux, .dll extension on Windows and .dylib extension on OSX. I need to switch on valgrind for test X an box Y temporarily to debug a problem. Support for SPARC in our preferred version of LLVM doesn't quite work yet. We need to use older version of LLVM on SPARC plarforms. And so on and so on. 8 | 9 | Trying to address this complexity by hand results in a big mess. Trying to address it via inheritance hierarchies doesn't work well either: Different dimensions don't aggregate in classic inheritance hierarchies, rather, they are composed in each-with-each combinatorial manner. 10 | 11 | ![](matrix.png) 12 | 13 | Cartesian is a simple Node module that generates such multidimensional configurations. It does so by allowing to define JavaScript objects with "alternative" properties. An alternative property can have multiple values. Such objects can then be expanded to an array of plain objects, a cartesian product of all the alternative properties: 14 | 15 | ```javascript 16 | var c = require('./cartesian.js') 17 | 18 | var obj = { 19 | a: 0, 20 | b: c.alt(1, 2), 21 | c: c.alt('A', 'B') 22 | } 23 | 24 | console.log(c.expand(obj)) 25 | ``` 26 | 27 | Call the file `example.js` and exexute it using Node: 28 | 29 | ``` 30 | $ node example.js 31 | ``` 32 | 33 | The output looks like this: 34 | 35 | ``` 36 | [ { a: 0, b: 1, c: 'A' }, 37 | { a: 0, b: 1, c: 'B' }, 38 | { a: 0, b: 2, c: 'A' }, 39 | { a: 0, b: 2, c: 'B' } ] 40 | ``` 41 | 42 | Let's now try to define a real-world configuration for a test suite. We can start by describing the boxes we have in our machine fleet: 43 | 44 | ```javascript 45 | var box1 = { 46 | hostname: 'box1', 47 | os: 'linux', 48 | arch: 'x86-64', 49 | ram: 8, 50 | } 51 | 52 | var box2 = { 53 | hostname: 'box2', 54 | os: 'freebsd', 55 | arch: 'arm', 56 | ram: 16, 57 | } 58 | 59 | var box3 = { 60 | hostname: 'box3', 61 | os: 'windows', 62 | arch: 'x86-64', 63 | ram: 4, 64 | } 65 | 66 | var box4 = { 67 | hostname: 'box4', 68 | os: 'illumos', 69 | arch: 'sparc', 70 | ram: 4, 71 | } 72 | ``` 73 | 74 | Here's the definition of the compilers: 75 | 76 | ```javascript 77 | var gcc = { 78 | binary: 'gcc', 79 | version: '4.8.4', 80 | } 81 | 82 | var clang = { 83 | binary: 'clang', 84 | version: '3.4.1', 85 | } 86 | 87 | var msvc = { 88 | binary: 'cl.exe', 89 | version: '15.00.30729.01', 90 | } 91 | ``` 92 | 93 | And finally, definitions of individual tests: 94 | 95 | ```javascript 96 | var frobnicate = { 97 | binary: 'frobnicate', 98 | sources: 'frobnicate.c' 99 | } 100 | 101 | var loadtest = { 102 | binary: 'loadtest', 103 | sources: 'loadtest.c helper.c' 104 | } 105 | 106 | var end2end = { 107 | binary: 'end2end', 108 | sources: 'end2end.c helper.c' 109 | } 110 | ``` 111 | 112 | Now we can combine all these dimensions into a single configuration: 113 | 114 | ```javascript 115 | var testsuite = { 116 | box: c.alt(box1, box2, box3, box4), 117 | compiler: c.alt(gcc, clang, msvc), 118 | test: c.alt(frobnicate, loadtest, end2end), 119 | } 120 | 121 | var config = c.expand(testsuite) 122 | console.log(JSON.stringify(config, null, ' ')) 123 | ``` 124 | 125 | The result is cartesian product of all the boxes, compilers and tests. There's a lot of results, so let's have a look only at the first and the laster one: 126 | 127 | ``` 128 | [ 129 | { 130 | "box": { 131 | "hostname": "box1", 132 | "os": "linux", 133 | "arch": "x86-64", 134 | "ram": 8 135 | }, 136 | "compiler": { 137 | "binary": "gcc", 138 | "version": "4.8.4" 139 | }, 140 | "test": { 141 | "binary": "frobnicate", 142 | "sources": "frobnicate.c" 143 | } 144 | }, 145 | 146 | ... 147 | 148 | { 149 | "box": { 150 | "hostname": "box4", 151 | "os": "illumos", 152 | "arch": "sparc", 153 | "ram": 4 154 | }, 155 | "compiler": { 156 | "binary": "cl.exe", 157 | "version": "15.00.30729.01" 158 | }, 159 | "test": { 160 | "binary": "end2end", 161 | "sources": "end2end.c helper.c" 162 | } 163 | } 164 | ] 165 | ``` 166 | 167 | One important thing to realize is that we can calculate new properties using classic JavaScript getter syntax. We may, for example, want a property that would contain the command line to run the test: 168 | 169 | ```javascript 170 | var testsuite = { 171 | box: c.alt(box1, box2, box3, box4), 172 | compiler: c.alt(gcc, clang, msvc), 173 | test: c.alt(frobnicate, loadtest, end2end), 174 | get cmdline() { 175 | return this.compiler.binary + ' ' + this.test.sources + ' -o ' + this.test.binary 176 | } 177 | } 178 | ``` 179 | 180 | The resulting configuration objects look like this: 181 | 182 | ``` 183 | { 184 | "box": { 185 | "hostname": "box1", 186 | "os": "linux", 187 | "arch": "x86-64", 188 | "ram": 8 189 | }, 190 | "compiler": { 191 | "binary": "gcc", 192 | "version": "4.8.4" 193 | }, 194 | "test": { 195 | "binary": "loadtest", 196 | "sources": "loadtest.c helper.c" 197 | }, 198 | "cmdline": "gcc loadtest.c helper.c -o loadtest" 199 | } 200 | ``` 201 | 202 | But wait! MSVC doesn't recognize -o option. We should use /Fe option instead. Let's do it be defining the option name in the compiler object: 203 | 204 | ```javascript 205 | var gcc = { 206 | binary: 'gcc', 207 | version: '4.8.4', 208 | output_option: '-o', 209 | } 210 | 211 | var clang = { 212 | binary: 'clang', 213 | version: '3.4.1', 214 | output_option: '-o', 215 | } 216 | 217 | var msvc = { 218 | binary: 'cl.exe', 219 | version: '15.00.30729.01', 220 | output_option: '/Fe', 221 | } 222 | 223 | ... 224 | 225 | var testsuite = { 226 | box: c.alt(box1, box2, box3, box4), 227 | compiler: c.alt(gcc, clang, msvc), 228 | test: c.alt(frobnicate, loadtest, end2end), 229 | get cmdline() { 230 | return this.compiler.binary + ' ' + this.test.sources + ' ' + 231 | this.compiler.output_option + ' ' + this.test.binary 232 | } 233 | } 234 | ``` 235 | 236 | Assuming that we will be adding more option names to compiler objects later on, it will be probably better to use a bit of inheritance and have all the options that are same for gcc and clang in the base class and overload them only for MSVC. This can be done using classic JavaScript inheritance: 237 | 238 | ```javascript 239 | var compiler = { 240 | output_option: '-o', 241 | } 242 | 243 | var gcc = { 244 | binary: 'gcc', 245 | version: '4.8.4', 246 | } 247 | gcc.__proto__ = compiler 248 | 249 | var clang = { 250 | binary: 'clang', 251 | version: '3.4.1', 252 | } 253 | clang.__proto__ = compiler 254 | 255 | var msvc = { 256 | binary: 'cl.exe', 257 | version: '15.00.30729.01', 258 | output_option: '/Fe', 259 | } 260 | msvc.__proto__ = compiler 261 | ``` 262 | 263 | Now it turns out there is a problem: Load test requires at least 8GB of memory and so it fails on boxes 3 and 4. But how are we supposed to get rid of the unwanted tests? To do this kind of stuff, Cartesian recognizes special object property called 'is'. If it evaluates to true the object will make it into the result. It it evaluates to false the object will be discarded: 264 | 265 | ```javascript 266 | var testsuite = { 267 | 268 | ... 269 | 270 | get is() { 271 | if(this.test.binary == 'loadtest' && this.box.ram < 8) return false 272 | return true 273 | } 274 | } 275 | ``` 276 | 277 | And that's all, folks. Cartesian can be used to produce for tests in a test suite, bunch of processes running on a machine, bunch of machines running in a cluster and so on. Enjoy! 278 | 279 | -------------------------------------------------------------------------------- /cartesian.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2017 Martin Sustrik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom 10 | the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | 23 | */ 24 | 25 | function alt() { 26 | var args = [] 27 | for(var i = 0; i < arguments.length; i++) args.push(arguments[i]) 28 | return {__alt__: args} 29 | } 30 | 31 | function expand(expr) { 32 | // primitive type 33 | if(typeof(expr) !== 'object') return [expr] 34 | // alt 35 | if('__alt__' in expr) { 36 | var res = [] 37 | for(var i = 0; i < expr.__alt__.length; i++) { 38 | var s = expand(expr.__alt__[i]) 39 | for(var j = 0; j < s.length; j++) res.push(s[j]) 40 | } 41 | return res 42 | } 43 | // array 44 | if(Array.isArray(expr)) { 45 | if(expr.length == 0) return [[]] 46 | var head = expand(expr[0]) 47 | var tail = expand(expr.slice(1)) 48 | var res = [] 49 | for(var i = 0; i < head.length; i++) { 50 | for(var j = 0; j < tail.length; j++) 51 | res.push([head[i]].concat(tail[j])) 52 | } 53 | return res 54 | } 55 | // object 56 | res = [{}] 57 | for(var name in expr) { 58 | // find the property descriptor, even if it is in a base class 59 | for(var it = expr; it != null; it = it.__proto__) { 60 | var desc = Object.getOwnPropertyDescriptor(it, name) 61 | if(desc != undefined) break 62 | } 63 | // getter functions are copied to the result 64 | if(desc.get != undefined) { 65 | for(var i = 0; i < res.length; i++) 66 | Object.defineProperty(res[i], name, desc) 67 | continue; 68 | } 69 | // child objects are recursively expanded 70 | var s = expand(expr[name]) 71 | var old = res 72 | res = [] 73 | for(var i = 0; i < old.length; i++) { 74 | for(var j = 0; j < s.length; j++) { 75 | var obj = {} 76 | for(var prop in old[i]) { 77 | var desc = Object.getOwnPropertyDescriptor(old[i], prop) 78 | Object.defineProperty(obj, prop, desc) 79 | } 80 | obj[name] = s[j] 81 | res.push(obj) 82 | } 83 | } 84 | } 85 | old = res 86 | res = [] 87 | for(var i = 0; i < old.length; i++) { 88 | // if 'is' property is false the object will not be part of the result 89 | if('is' in old[i] && !old[i].is) continue 90 | // 'is' itself is not in the result as it would be always true anyway 91 | delete old[i].is 92 | // evaluate any getter functions (poor-man's memoization) 93 | for(name in old[i]) 94 | Object.defineProperty(old[i], name, {value: old[i][name]}) 95 | res.push(old[i]); 96 | } 97 | return res 98 | } 99 | 100 | exports.alt = alt 101 | exports.expand = expand 102 | 103 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 2 | var c = require('./cartesian.js') 3 | 4 | var box1 = { 5 | hostname: 'box1', 6 | os: 'linux', 7 | arch: 'x86-64', 8 | ram: 8, 9 | } 10 | 11 | var box2 = { 12 | hostname: 'box2', 13 | os: 'freebsd', 14 | arch: 'arm', 15 | ram: 16, 16 | } 17 | 18 | var box3 = { 19 | hostname: 'box3', 20 | os: 'windows', 21 | arch: 'x86-64', 22 | ram: 4, 23 | } 24 | 25 | var box4 = { 26 | hostname: 'box4', 27 | os: 'illumos', 28 | arch: 'sparc', 29 | ram: 4, 30 | } 31 | 32 | var compiler = { 33 | output_option: '-o', 34 | } 35 | 36 | var gcc = { 37 | binary: 'gcc', 38 | version: '4.8.4', 39 | } 40 | gcc.__proto__ = compiler 41 | 42 | var clang = { 43 | binary: 'clang', 44 | version: '3.4.1', 45 | } 46 | clang.__proto__ = compiler 47 | 48 | var msvc = { 49 | binary: 'cl.exe', 50 | version: '15.00.30729.01', 51 | output_option: '/Fe', 52 | } 53 | msvc.__proto__ = compiler 54 | 55 | var frobnicate = { 56 | binary: 'frobnicate', 57 | sources: 'frobnicate.c' 58 | } 59 | 60 | var loadtest = { 61 | binary: 'loadtest', 62 | sources: 'loadtest.c helper.c' 63 | } 64 | 65 | var end2end = { 66 | binary: 'end2end', 67 | sources: 'end2end.c helper.c' 68 | } 69 | 70 | var testsuite = { 71 | box: c.alt(box1, box2, box3, box4), 72 | compiler: c.alt(gcc, clang, msvc), 73 | test: c.alt(frobnicate, loadtest, end2end), 74 | get cmdline() { 75 | return this.compiler.binary + ' ' + this.test.sources + ' ' + 76 | this.compiler.output_option + ' ' + this.test.binary 77 | }, 78 | get is() { 79 | if(this.test.binary == 'loadtest' && this.box.ram < 8) return false 80 | return true 81 | } 82 | } 83 | 84 | var config = c.expand(testsuite) 85 | console.log(JSON.stringify(config, null, ' ')) 86 | 87 | -------------------------------------------------------------------------------- /matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sustrik/cartesian/3877fcdf813a071b96d3dfa030857047fcc68747/matrix.png --------------------------------------------------------------------------------