001 /*
002 * Copyright (c) 2005 Jens Schou, Staffan Gustafsson, Björn Lanneskog,
003 * Einar Pehrson and Sebastian Kekkonen
004 *
005 * This file is part of
006 * CleanSheets Extension for Test Cases
007 *
008 * CleanSheets Extension for Test Cases is free software; you can
009 * redistribute it and/or modify it under the terms of the GNU General Public
010 * License as published by the Free Software Foundation; either version 2 of
011 * the License, or (at your option) any later version.
012 *
013 * CleanSheets Extension for Test Cases is distributed in the hope that
014 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied
015 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
016 * See the GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with CleanSheets Extension for Test Cases; if not, write to the
020 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
021 * Boston, MA 02111-1307 USA
022 */
023 package csheets.ext.test;
024
025 import java.io.IOException;
026 import java.util.ArrayList;
027 import java.util.HashMap;
028 import java.util.HashSet;
029 import java.util.Iterator;
030 import java.util.List;
031 import java.util.Map;
032 import java.util.Set;
033 import java.util.SortedSet;
034
035 import csheets.core.Cell;
036 import csheets.core.Value;
037 import csheets.ext.CellExtension;
038
039 /**
040 * An extension of a cell in a spreadsheet, with support for test cases.
041 * @author Staffan Gustafsson
042 * @author Jens Schou
043 * @author Einar Pehrson
044 */
045 public class TestableCell extends CellExtension {
046
047 /** The unique version identifier used for serialization */
048 private static final long serialVersionUID = -2626239432851585308L;
049
050 /** The cell's test case parameters */
051 private Set<TestCaseParam> tcParams = new HashSet<TestCaseParam>();
052
053 /** The cell's test cases */
054 private Set<TestCase> testCases = new HashSet<TestCase>();
055
056 /** The listeners registered to receive events from the testable cell */
057 private transient List<TestableCellListener> listeners
058 = new ArrayList<TestableCellListener>();
059
060 /**
061 * Creates a testable cell extension for the given cell.
062 * @param cell the cell to extend
063 */
064 TestableCell(Cell cell) {
065 super(cell, TestExtension.NAME);
066 }
067
068
069 /*
070 * DATA UPDATES
071 */
072
073
074 /**
075 * Invoked to indicate that the content of the cell in the spreadsheet was
076 * modified and that test cases and test case paremeters that depend on that
077 * data must be updated, and new ones generated.
078 */
079 public void contentChanged(Cell cell) {
080 if (getFormula() != null) {
081 resetTestCases();
082 } else {
083 removeAllTcpsOfType(TestCaseParam.Type.DERIVED);
084 testCases.clear();
085 }
086 }
087
088
089 /*
090 * TEST CASE ACCESSORS
091 */
092
093
094 /**
095 * Returns the test cases for the cell, which consist of a predetermined
096 * value for each of the cell's precedents.
097 * @return the cell's test cases
098 */
099 public Set<TestCase> getTestCases(){
100 return testCases;
101 }
102
103 /**
104 * Returns whether the cell has any test cases.
105 * @return true if the cell has any test cases
106 */
107 public boolean hasTestCases(){
108 return !testCases.isEmpty();
109 }
110
111 /**
112 * Returns whether any of the cell's test cases have been rejected.
113 * @return true if any of the cell's test cases have been rejected
114 */
115 public boolean hasTestError() {
116 for (TestCase testCase : testCases)
117 if (testCase.getValidationState() == TestCase.ValidationState.REJECTED)
118 return true;
119 return false;
120 }
121
122 /**
123 * Returns the testedness of the cell, i.e. the ratio of valid
124 * test cases to available test cases in the cell.
125 * @return a number between 0.0 and 1.0 denoting the level of testedness
126 */
127 public double getTestedness() {
128 if (hasTestCases()) {
129 // Calculates and returns the testedness
130 double nValid = 0;
131 for (TestCase testCase : testCases)
132 if (testCase.getValidationState() == TestCase.ValidationState.VALID)
133 nValid++;
134 return nValid / testCases.size();
135 } else
136 return 0d;
137 }
138
139
140 /*
141 * TEST CASE MODIFIERS
142 */
143
144
145 /**
146 * Generates new test cases for the cell, provided that all its precedents
147 * have test case parameters.
148 */
149 public void resetTestCases(){
150 boolean changed = false;
151
152 if(!testCases.isEmpty()) {
153 testCases.clear();
154 removeAllTcpsOfType(TestCaseParam.Type.DERIVED);
155 changed = true;
156 }
157
158 if(allPrecedentsHaveParams() && getPrecedents().size() > 0) {
159 // We pick one precedent at random to initiate the set
160 TestableCell firstPrec = (TestableCell)getPrecedents().first();
161 Iterator<TestCaseParam> paramIt = firstPrec.getTestCaseParams().iterator();
162 // make one extention in the set per parameter
163 while(paramIt.hasNext()) {
164 //extendTestCases takes care of the rest of the precedents
165 extendTestCases(firstPrec, paramIt.next());
166 }
167 changed = true;
168 }
169
170 if(changed) {
171 fireTestCasesChanged();
172 }
173 }
174
175 protected void extendTestCases(TestableCell firstPrec, TestCaseParam param) {
176 SortedSet<Cell> precedents = getPrecedents();
177 precedents.remove(firstPrec);
178
179 // The first precedent initiates the set
180 // make one entry in the set for the parameter
181 Map<Cell, Value> caseMap = new HashMap<Cell, Value>();
182 caseMap.put(firstPrec, param.getValue());
183
184 Set<Map<Cell, Value>> casesSet = createCasesSet(precedents, caseMap);
185
186 if(toTestCases(casesSet))
187 fireTestCasesChanged();
188 }
189
190 private Set<Map<Cell, Value>> createCasesSet(Set<Cell> precedents,
191 Map<Cell, Value> caseMap){
192 // Set to store all maps used to make test cases
193 Set<Map<Cell, Value>> casesSet = new HashSet<Map<Cell, Value>>();
194 casesSet.add(caseMap);
195
196 // Now, update casesSet for each precedent
197 for(Cell prec : precedents){
198 // a temporary set to store new caseMaps during the iteration
199 Set<Map<Cell, Value>> tempCasesSet
200 = new HashSet<Map<Cell, Value>>();
201
202 for(TestCaseParam precParam : ((TestableCell)prec).getTestCaseParams()){
203 // for each test case param in the precedent
204 for(Map<Cell, Value> item : casesSet){
205
206 // for every caseMap
207 // make a copy, add current precedent address and param
208 Map<Cell, Value> itemCopy = new HashMap<Cell, Value>();
209 itemCopy.putAll(item);
210 itemCopy.put(precParam.getCell(), precParam.getValue());
211 // add the copy to tempCasesSet
212 tempCasesSet.add(itemCopy);
213 }
214 }
215 casesSet = tempCasesSet;
216 }
217 return casesSet;
218 }
219
220 private boolean toTestCases(Set<Map<Cell, Value>> casesSet){
221 boolean tcChanged = false;
222 // for every item in casesSet, make TestCase and add to testCases
223
224 for(Map<Cell, Value> aoMap : casesSet){
225 Set<TestCaseParam> tcParams = new HashSet<TestCaseParam>();
226 Set<Map.Entry<Cell, Value>> aoSet = aoMap.entrySet();
227
228 for(Map.Entry<Cell, Value> entry : aoSet){
229 tcParams.add(new TestCaseParam(
230 (TestableCell)entry.getKey().getExtension(TestExtension.NAME),
231 entry.getValue(), TestCaseParam.Type.DERIVED));
232 }
233
234 // Creates the test case
235 TestCase testCase = new TestCase(this, tcParams);
236 testCases.add(testCase);
237 tcChanged = true;
238 }
239 return tcChanged;
240 }
241
242
243 /*
244 * TEST CASE PARAMETER ACCESSORS
245 */
246
247
248 /**
249 * Returns the cell's test case parameters.
250 * @return the cell's the test case parameters.
251 */
252 public Set<TestCaseParam> getTestCaseParams(){
253 return tcParams;
254 }
255
256 /**
257 * Returns whether the cell has any test case parameters.
258 * @return true if the cell has any test case parameters
259 */
260 public boolean hasTestCaseParams(){
261 return !tcParams.isEmpty();
262 }
263
264 /**
265 * Tests if all of the cells precedents have test case parameters.
266 * @return true if all of the cells precedents have test case parameters
267 */
268 protected boolean allPrecedentsHaveParams(){
269 for (Cell precedent : getPrecedents())
270 if (!((TestableCell)precedent).hasTestCaseParams())
271 return false;
272 return true;
273 }
274
275 /*
276 * TEST CASE PARAMETER MODIFIERS
277 */
278
279
280 /**
281 * Add a test case parameter to the cell's set of test case parameters.
282 * On addition, the cell's dependents are notified.
283 * @param value the value of the test case parameter to be added
284 * @return the parameter that was added, or null if the cell already had an identical parameter
285 */
286 public TestCaseParam addTestCaseParam(Value value) throws DuplicateUserTCPException {
287
288 TestCaseParam param = null;
289 Iterator<TestCaseParam> it = tcParams.iterator();
290 while(it.hasNext()) {
291 param = it.next();
292 if(value.equals(param.getValue())) {
293 if(param.isUserEntered()) {
294 throw new DuplicateUserTCPException(value,
295 "Cells cannot have duplicate user-entered test case parameters");
296 }
297 else {
298 param.setType(TestCaseParam.Type.USER_ENTERED, true);
299 return param;
300 }
301 }
302 }
303 return addTestCaseParam(value, TestCaseParam.Type.USER_ENTERED);
304 }
305
306 /**
307 * Add a test case parameter to the cell's set of test case parameters.
308 * On addition, the cell's dependents are notified.
309 * @param value the value of the test case parameter to be added
310 * @param type the type of test case parameter
311 */
312 public TestCaseParam addTestCaseParam(Value value, TestCaseParam.Type type) {
313
314 TestCaseParam param = null;
315 Iterator<TestCaseParam> it = tcParams.iterator();
316 while(it.hasNext()) {
317 param = it.next();
318 if(value.equals(param.getValue())) {
319 param.setType(type, true);
320 return param;
321 }
322 }
323 param = new TestCaseParam(this, value, type);
324 tcParams.add(param);
325 for (Cell dependent : getDependents()) {
326 ((TestableCell)dependent).precedentAddedParam(this, param);
327 }
328 // Notifies listeners
329 fireTestCaseParametersChanged();
330
331 return param;
332 }
333
334 /**
335 * Removes a test case parameter from the cell's set of test case parameters.
336 * On removal, the cell's dependents are notified.
337 * @param param the test case parameter to be removed
338 */
339 public void removeTestCaseParam(TestCaseParam param) {
340 removeTestCaseParam(param, TestCaseParam.Type.USER_ENTERED);
341 }
342
343 /**
344 * Removes a test case parameter from the cell's set of test case parameters.
345 * On removal, the cell's dependents are notified.
346 * @param param the test case parameter to be removed
347 * @param type the type of the parameter to remove
348 */
349 public void removeTestCaseParam(TestCaseParam param, TestCaseParam.Type type) {
350
351 // hitta param, toggla av type
352 param.setType(type, false);
353 // om param har inga type -> ta bort och meddela dependents
354 if(param.hasNoType()) {
355 tcParams.remove(param);
356
357 // Notifies the cell's dependents
358 for (Cell dependent : getDependents()){
359 ((TestableCell)dependent).precedentRemovedParam(this, param);
360 }
361 // Notifies listeners
362 fireTestCaseParametersChanged();
363 }
364 }
365
366 protected void removeAllTcpsOfType(TestCaseParam.Type type) {
367 Iterator<TestCaseParam> tcpIt = tcParams.iterator();
368 // for all params...
369 while(tcpIt.hasNext()){
370 TestCaseParam tcp = tcpIt.next();
371 // ...check those of the specified type
372 tcp.setType(type, false);
373 if(tcp.hasNoType()){
374 tcpIt.remove();
375
376 // ...for all dependents...
377 for (Cell dependent : getDependents())
378 // ...I no longer have this param
379 ((TestableCell)dependent).precedentRemovedParam(this, tcp);
380
381 // Notifies listeners
382 fireTestCaseParametersChanged();
383 }
384 }
385 }
386
387
388 /*
389 * TEST CASE PARAMETER UPDATES
390 */
391
392
393 /**
394 * Invoked when a test case parameter is added to one of the cell's
395 * precedents. This causes the cell's test cases to be updated.
396 * @param cell the precedent to which the test case parameter was added
397 * @param param the test case parameter that was added
398 */
399 public void precedentAddedParam(TestableCell cell, TestCaseParam param) {
400 /*
401 * We only need to do anything if all our precedents have params
402 */
403 if (allPrecedentsHaveParams()) {
404 /*
405 * if we don't have any test cases, we just make a whole new set
406 */
407 if(testCases.isEmpty())
408 resetTestCases();
409 /*
410 * if test cases exist, we want to keep the old, and just update
411 * with the new test cases generated by the new param
412 */
413 else {
414 extendTestCases(cell, param);
415 }
416 }
417 }
418
419 /**
420 * Invoked when a test case parameter is removed from one of the cell's
421 * precedents. This causes the cell's test cases to be updated.
422 * @param cell the precedent from which the test case parameter was removed
423 * @param param the test case parameter that was removed
424 */
425 public void precedentRemovedParam(TestableCell cell, TestCaseParam param){
426 /*
427 * if all precedents still have params, just remove the test cases
428 * pertaining to the removed parameter.
429 */
430 if(allPrecedentsHaveParams()){
431 // iterate the test cases.
432 Iterator<TestCase> tcIt = testCases.iterator();
433
434 // remove all test cases that used param as a parameter:
435 while(tcIt.hasNext()){
436 TestCase tCase = tcIt.next();
437 Set<TestCaseParam> paramMap = tCase.getParams();
438 if(paramMap.contains(param)){ // testcase uses removed param
439 tcIt.remove(); // remove the test case
440 TestCaseParam derivedParam = null;
441 Iterator<TestCaseParam> it = tcParams.iterator();
442 while(it.hasNext()) {
443 derivedParam = it.next();
444 if(tCase.evaluate().equals(derivedParam.getValue()))
445 break;
446 }
447 if(derivedParam != null)
448 removeTestCaseParam(derivedParam, TestCaseParam.Type.DERIVED);
449 }
450 }
451 fireTestCasesChanged();
452 }
453 /* If some precedents lack params, our dependents need to be notified
454 * that all our derived params no longer apply (except for the derived
455 * params that happen to be the same as a local param).
456 * (no need to notify if we don't have any test cases, since then we
457 * dn't have any derived params either) */
458 else if(!testCases.isEmpty()){
459 testCases.clear();
460 // inga test cases -> inga derived test case params
461 removeAllTcpsOfType(TestCaseParam.Type.DERIVED);
462 fireTestCasesChanged();
463 }
464 }
465
466
467 /*
468 * CLIPBOARD
469 */
470
471
472 /**
473 * Removes the test case parameters from the cell.
474 * @param cell the cell that was modified
475 */
476 public void cellCleared(Cell cell) {
477 if (this.getDelegate().equals(cell)) {
478 tcParams.clear();
479 }
480 }
481
482 /**
483 * Copies the user-specified test case parameters from the source cell to
484 * this one.
485 * @param cell the cell that was modified
486 * @param source the cell from which data was copied
487 */
488 public void cellCopied(Cell cell, Cell source) {
489 if (this.getDelegate().equals(cell)) {
490 TestableCell testableSource = (TestableCell)source.getExtension(
491 TestExtension.NAME);
492 tcParams.clear();
493 for (TestCaseParam param : testableSource.getTestCaseParams())
494 if (param.hasType(TestCaseParam.Type.USER_ENTERED))
495 try {
496 addTestCaseParam(param.getValue());
497 } catch (DuplicateUserTCPException e) {}
498 }
499 }
500
501
502 /*
503 * EVENT LISTENING SUPPORT
504 */
505
506
507 /**
508 * Registers the given listener on the cell.
509 * @param listener the listener to be added
510 */
511 public void addTestableCellListener(TestableCellListener listener) {
512 listeners.add(listener);
513 }
514
515 /**
516 * Removes the given listener from the cell.
517 * @param listener the listener to be removed
518 */
519 public void removeTestableCellListener(TestableCellListener listener) {
520 listeners.remove(listener);
521 }
522
523 /**
524 * Notifies all registered listeners that the cell's test cases changed.
525 */
526 protected void fireTestCasesChanged() {
527 for (TestableCellListener listener : listeners)
528 listener.testCasesChanged(this);
529 }
530
531 /**
532 * Notifies all registered listeners that the cell's test case parameters changed.
533 */
534 protected void fireTestCaseParametersChanged() {
535 for (TestableCellListener listener : listeners)
536 listener.testCaseParametersChanged(this);
537 }
538
539 /**
540 * Customizes serialization, by recreating the listener list.
541 * @param stream the object input stream from which the object is to be read
542 * @throws IOException If any of the usual Input/Output related exceptions occur
543 * @throws ClassNotFoundException If the class of a serialized object cannot be found.
544 */
545 private void readObject(java.io.ObjectInputStream stream)
546 throws java.io.IOException, ClassNotFoundException {
547 stream.defaultReadObject();
548 listeners = new ArrayList<TestableCellListener>();
549 }
550 }