Source: org/terraswarm/accessor/accessors/web/test/TrainableTest.js

  1. // Copyright (c) 2016-2017 The Regents of the University of California.
  2. // All rights reserved.
  3. //
  4. // Permission is hereby granted, without written agreement and without
  5. // license or royalty fees, to use, copy, modify, and distribute this
  6. // software and its documentation for any purpose, provided that the above
  7. // copyright notice and the following two paragraphs appear in all copies
  8. // of this software.
  9. //
  10. // IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
  11. // FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
  12. // ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
  13. // THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
  14. // SUCH DAMAGE.
  15. //
  16. // THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
  17. // INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  18. // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
  19. // PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
  20. // CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
  21. // ENHANCEMENTS, OR MODIFICATIONS.
  22. //
  23. /** Compare the input with a known good input.
  24. * If you set ''trainingMode'' to true and provide inputs, then the
  25. * inputs will be recorded in the ''correctValues'' parameters.
  26. * Otherwise, the inputs will be compared against those in the
  27. * ''correctValue'' parameter.
  28. *
  29. * @accessor test/TrainableTest
  30. * @input input The input value.
  31. * @output output False as long as there is data to compare against the input
  32. * @param correctValues a JSON array of the correct values.
  33. * @param trainingMode true if the input is being trained.
  34. * @author Christopher Brooks based on the Ptolemy NonStrictTest actor by Paul Whitaker, Christopher Hylands, Edward A. Lee
  35. * @version $$Id$$
  36. */
  37. // Stop extra messages from jslint and jshint. Note that there should
  38. // be no space between the / and the * and global. See
  39. // https://chess.eecs.berkeley.edu/ptexternal/wiki/Main/JSHint */
  40. /*globals console, exports*/
  41. /*jshint globalstrict: true*/
  42. /*jslint plusplus: true */
  43. 'use strict';
  44. exports.setup = function () {
  45. this.parameter('correctValues', {
  46. 'value': [0]
  47. });
  48. this.input('input');
  49. this.output('output', {
  50. 'type': 'boolean'
  51. });
  52. this.parameter('tolerance', {
  53. 'type': 'number',
  54. 'value': 0.000000001
  55. });
  56. this.parameter('trainingMode', {
  57. 'type': 'boolean',
  58. 'value': false
  59. });
  60. };
  61. // Input, parameter and variable names match those in $PTII/ptolemy/actor/lib/NonStrictTest.java
  62. // Set to true if an input is handled. If no inputs are handled, then
  63. // throw an exception in wrapup().
  64. var inputHandled = false;
  65. // Set to true when initialize() is called.
  66. var initialized = false;
  67. // The number of input tokens that have been read in.
  68. var numberOfInputTokensSeen = 0;
  69. // If trainingMode is true, then inputs that have been seen so far.
  70. var trainingTokens = [];
  71. // Set to false in initialize() and true at the end of wrapup().
  72. // FIXME: We should have an exit hook that checks that wrapup() is called for all the actors.
  73. var wrappedUp = false;
  74. // So we can test this in hosts/node/test/mocha/testMain.js to test that wrapup was called.
  75. exports.wrappedUp = wrappedUp;
  76. // Return true if the object has the same properties, in any order.
  77. // Based on http://procbits.com/2012/01/19/comparing-two-javascript-objects
  78. var objectPropertiesEqual = function(object1, object2) {
  79. var property;
  80. // Check that all the properties in object2 are present in object.
  81. for ( property in object2) {
  82. if (typeof object1[property] === 'undefined') {
  83. return false;
  84. }
  85. }
  86. // Check that all the properties in object1 are preset in object2.
  87. for (property in object1) {
  88. if (typeof object2[property] === 'undefined') {
  89. return false;
  90. }
  91. }
  92. // If a property is an object1, the recursively call this function.
  93. // If a property is a function, then do a string comparison.
  94. for (property in object2) {
  95. if (object2[property]) {
  96. switch (typeof object2[property]) {
  97. case 'object1':
  98. // Here's the recursive bit
  99. if (!objectPropertiesEqual(object1[property], object2[property])) {
  100. return false;
  101. }
  102. break;
  103. case 'function':
  104. if (typeof object1[property] ==='undefined' ||
  105. (property != 'object1PropertiesEqual' &&
  106. object2[property].toString() != object1[property].toString())) {
  107. return false;
  108. }
  109. break;
  110. default:
  111. if (object2[property] !== object1[property]) {
  112. return false;
  113. }
  114. }
  115. } else {
  116. // FIXME: I'm not sure if this case is ever used, but it was in
  117. // http://procbits.com/2012/01/19/comparing-two-javascript-objects
  118. if (object1[property]) {
  119. return false;
  120. }
  121. }
  122. }
  123. return true;
  124. };
  125. /** Create an input handler to compare the input with the appropriate element(s)
  126. * from correctValues.
  127. */
  128. exports.initialize = function () {
  129. //console.log("Test initialize(): typeof correctValues: " + typeof this.getParameter('correctValues'))
  130. var inputValueValue,
  131. self = this;
  132. trainingTokens = [];
  133. exports.wrappedUp = false;
  134. numberOfInputTokensSeen = 0;
  135. this.addInputHandler('input', function () {
  136. var cache = [],
  137. inputValue = self.get('input'),
  138. inputValueValue;
  139. inputHandled = true;
  140. // If the input is not connected, then inputValue will be null.
  141. if (self.getParameter('trainingMode')) {
  142. trainingTokens.push(inputValue);
  143. self.send('output', false);
  144. return;
  145. }
  146. var correctValuesValues = self.getParameter('correctValues');
  147. if (numberOfInputTokensSeen < correctValuesValues.length) {
  148. var referenceToken = correctValuesValues[numberOfInputTokensSeen];
  149. //console.log("Test: " + numberOfInputTokensSeen + ", input: " + inputValue
  150. //+ ", referenceToken: " + referenceToken);
  151. if (typeof inputValue !== 'boolean' &&
  152. typeof inputValue !== 'number' &&
  153. typeof inputValue !== 'object' &&
  154. typeof inputValue !== 'string') {
  155. if (inputValue === null) {
  156. throw new Error(self.accessorName + ': After seeing ' + numberOfInputTokensSeen +
  157. ' tokens, the value of the input was null? ' +
  158. 'Perhaps the input is not connected?'
  159. );
  160. }
  161. cache = [];
  162. inputValueValue = JSON.stringify(inputValue, function (key, value) {
  163. if (typeof value === 'object' && value !== null) {
  164. if (cache.indexOf(value) !== -1) {
  165. // Circular reference found, discard key
  166. return;
  167. }
  168. // Store value in our collection
  169. cache.push(value);
  170. }
  171. return value;
  172. });
  173. if (inputValueValue.length > 100) {
  174. inputValueValue = inputValueValue.substring(0, 100) + '...';
  175. }
  176. cache = null; // Enable garbage collection
  177. throw new Error(self.accessorName + ': After seeing ' + numberOfInputTokensSeen +
  178. ' tokens, the input "' + inputValue +
  179. '" is neither a number nor a string, it is a ' +
  180. typeof inputValue + ' with value ' + inputValueValue);
  181. }
  182. if (typeof referenceToken === 'boolean') {
  183. // If the input not a boolean, then throw an error.
  184. if (typeof inputValue !== 'boolean') {
  185. inputValueValue = inputValue;
  186. if (typeof inputValue === 'object') {
  187. inputValueValue = JSON.stringify(inputValue, function (key, value) {
  188. if (typeof value === 'object' && value !== null) {
  189. if (cache.indexOf(value) !== -1) {
  190. // Circular reference found, discard key
  191. return;
  192. }
  193. // Store value in our collection
  194. cache.push(value);
  195. }
  196. return value;
  197. });
  198. }
  199. throw new Error(self.accessorName + ': After seeing ' + numberOfInputTokensSeen +
  200. ' tokens, the input "' + inputValueValue +
  201. '" is not a boolean, it is a ' +
  202. typeof inputValue + '. The expected value was "' +
  203. referenceToken + '"');
  204. }
  205. if (inputValue !== referenceToken) {
  206. throw new Error(self.accessorName + ': After seeing ' + numberOfInputTokensSeen +
  207. ' tokens, the input "' + inputValue + '" is not equal to "' +
  208. referenceToken + '"');
  209. }
  210. } else if (typeof referenceToken === 'number') {
  211. // If the input not a number, then throw an error.
  212. if (typeof inputValue !== 'number') {
  213. inputValueValue = inputValue;
  214. if (typeof inputValue === 'object') {
  215. inputValueValue = JSON.stringify(inputValue, function (key, value) {
  216. if (typeof value === 'object' && value !== null) {
  217. if (cache.indexOf(value) !== -1) {
  218. // Circular reference found, discard key
  219. return;
  220. }
  221. // Store value in our collection
  222. cache.push(value);
  223. }
  224. return value;
  225. });
  226. }
  227. throw new Error(self.accessorName + ': After seeing ' + numberOfInputTokensSeen +
  228. ' tokens, the input "' + inputValueValue +
  229. '" is not a number, it is a ' +
  230. typeof inputValue + '. The expected value was "' +
  231. referenceToken + '"');
  232. }
  233. var difference = Math.abs(inputValue - referenceToken);
  234. if (isNaN(difference)) {
  235. throw new Error(self.accessorName + ': After seeing ' + numberOfInputTokensSeen +
  236. ' tokens, the absolute value of the input "' +
  237. inputValue + '" - the referenceToken "' +
  238. referenceToken + '" is NaN? It should be less than ' +
  239. self.getParameter('tolerance'));
  240. }
  241. if (difference > self.getParameter('tolerance')) {
  242. throw new Error(self.accessorName + ': After seeing ' + numberOfInputTokensSeen +
  243. ' tokens, the input "' + inputValue + '" is not within "' +
  244. self.getParameter('tolerance') +
  245. '" of the expected value "' +
  246. referenceToken + '"');
  247. }
  248. } else if (typeof referenceToken === 'string') {
  249. if (inputValue !== referenceToken) {
  250. // devices/test/auto/WatchEmulator.js needs this test for object because
  251. // if we receive a JSON object, then we should try to stringify it.
  252. if (typeof inputValue === 'object') {
  253. inputValueValue = null;
  254. try {
  255. inputValueValue = JSON.stringify(inputValue);
  256. } catch (err) {
  257. throw new Error(self.accessorName + ': After seeing ' + numberOfInputTokensSeen +
  258. ' tokens, the input "' + inputValue + '" is !== ' +
  259. ' to the expected value "' +
  260. referenceToken + '". The input was an object, and a string was expected.');
  261. }
  262. if (inputValueValue !== referenceToken) {
  263. throw new Error(self.accessorName + ': After seeing ' + numberOfInputTokensSeen +
  264. ' tokens, the input "' + inputValueValue + '" is !== ' +
  265. ' to the expected value "' +
  266. referenceToken +
  267. '". The input was an object and JSON.stringify() did not throw an exception.' +
  268. 'A string was expected.');
  269. }
  270. }
  271. }
  272. } else if (typeof referenceToken === 'object') {
  273. // Sadly, in JavaScript, objects that have the same
  274. // properties, but in a different order are not
  275. // consider equal in that Object.is() will return
  276. // false. However, Ptolemy RecordTokens are by
  277. // default unordered (unless they are
  278. // OrderedRecordTokens), So, we have a function that
  279. // does a deep comparison and ignores differences in
  280. // property order.
  281. if (objectPropertiesEqual(inputValue, referenceToken)) {
  282. // The objects are not the same.
  283. // Generate string representations of the values
  284. // so that the user can possibly tell what went
  285. // wrong.
  286. cache = [];
  287. inputValueValue = JSON.stringify(inputValue, function (key, value) {
  288. if (typeof value === 'object' && value !== null) {
  289. if (cache.indexOf(value) !== -1) {
  290. // Circular reference found, discard key
  291. return;
  292. }
  293. // Store value in our collection
  294. cache.push(value);
  295. }
  296. return value;
  297. });
  298. cache = [];
  299. var referenceTokenValue = JSON.stringify(referenceToken, function (key, value) {
  300. if (typeof value === 'object' && value !== null) {
  301. if (cache.indexOf(value) !== -1) {
  302. // Circular reference found, discard key
  303. return;
  304. }
  305. // Store value in our collection
  306. cache.push(value);
  307. }
  308. return value;
  309. });
  310. cache = null; // Enable garbage collection
  311. // If we are comparing longs from CapeCode, then the values will be like "1L",
  312. // and stringify will return undefined.
  313. if (inputValueValue === undefined) {
  314. inputValueValue = inputValue;
  315. }
  316. if (referenceTokenValue === undefined) {
  317. referenceTokenValue = referenceToken;
  318. }
  319. if (inputValueValue !== referenceTokenValue) {
  320. // inputValueValue could still be undefined here if inputValue
  321. // was undefined.
  322. if (inputValueValue !== undefined && inputValueValue.length > 100) {
  323. inputValueValue = inputValueValue.substring(0, 100) + '...';
  324. }
  325. if (referenceTokenValue !== undefined && referenceTokenValue.length > 100) {
  326. referenceTokenValue = referenceTokenValue.substring(0, 100) + '...';
  327. }
  328. // Deal with referenceTokens with value 1L.
  329. if (typeof inputValueValue !== 'object' || typeof referenceTokenValue !== 'object' &&
  330. inputValueValue.toString() !== referenceTokenValue.toString) {
  331. throw new Error(self.accessorName + ': After seeing ' + numberOfInputTokensSeen +
  332. ' tokens, the input Object \n"' + inputValueValue +
  333. '" is !== to the expected value Object\n"' +
  334. referenceTokenValue);
  335. }
  336. }
  337. }
  338. } else {
  339. throw new Error(self.accessorName + ': After seeing ' + numberOfInputTokensSeen +
  340. ' tokens, the referenceToken "' + referenceToken +
  341. '" is not a number, it is a ' +
  342. typeof referenceToken);
  343. }
  344. numberOfInputTokensSeen += 1;
  345. // If we are past the end of the expected inputs, then read
  346. if (numberOfInputTokensSeen >= correctValuesValues.length) {
  347. self.send('output', true);
  348. } else {
  349. self.send('output', false);
  350. }
  351. } else {
  352. self.send('output', true);
  353. }
  354. });
  355. initialized = true;
  356. };
  357. /** If trainingMode is true, then updated the correctValues. */
  358. exports.wrapup = function () {
  359. if (this.getParameter('trainingMode')) {
  360. this.setParameter('correctValues', trainingTokens);
  361. } else {
  362. if (initialized) {
  363. if (!inputHandled) {
  364. initialized = false;
  365. throw new Error(this.accessorName + ': The input handler of this accessor was never invoked. ' +
  366. 'Usually, this is an error indicating that ' +
  367. 'starvation is occurring.');
  368. }
  369. var correctValuesValues = this.getParameter('correctValues');
  370. if (numberOfInputTokensSeen < correctValuesValues.length) {
  371. throw new Error(this.accessorName + ': The test produced only ' +
  372. numberOfInputTokensSeen +
  373. ' tokens, yet the correctValues parameter was ' +
  374. 'expecting ' +
  375. correctValuesValues.length +
  376. ' tokens');
  377. }
  378. }
  379. initialized = false;
  380. }
  381. var name = this.accessorName;
  382. // FIXME: Should we check to see if the name has no dots in and if
  383. // it does not, add the container name?
  384. //if (this.container) {
  385. // name = this.container.accessorName + "." + name;
  386. //}
  387. //
  388. exports.wrappedUp = true;
  389. // console.log("TrainableTest.js: wrapup() finished: " + name + ", exports.wrappedUp: " + exports.wrappedUp);
  390. };