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    }