001/* 002 * Copyright (c) 2005 Jens Schou, Staffan Gustafsson, Bjorn 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 */ 023package csheets.ext.test; 024 025import java.io.IOException; 026import java.util.ArrayList; 027import java.util.HashMap; 028import java.util.HashSet; 029import java.util.Iterator; 030import java.util.List; 031import java.util.Map; 032import java.util.Set; 033import java.util.SortedSet; 034 035import csheets.core.Cell; 036import csheets.core.Value; 037import 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 */ 045public 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}