001: /* Copyright (c) 2001-2005, The HSQL Development Group
002: * All rights reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * Redistributions of source code must retain the above copyright notice, this
008: * list of conditions and the following disclaimer.
009: *
010: * Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: *
014: * Neither the name of the HSQL Development Group nor the names of its
015: * contributors may be used to endorse or promote products derived from this
016: * software without specific prior written permission.
017: *
018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
020: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
021: * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
022: * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025: * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
026: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029: */
030:
031: package org.hsqldb.test;
032:
033: import java.io.File;
034: import java.io.FileReader;
035: import java.io.LineNumberReader;
036: import java.sql.Connection;
037: import java.sql.ResultSet;
038: import java.sql.SQLException;
039: import java.sql.Statement;
040:
041: import org.hsqldb.lib.HsqlArrayList;
042: import org.hsqldb.lib.StringUtil;
043:
044: /**
045: * Utility class providing methodes for submitting test statements or
046: * scripts to the database, comparing the results returned with
047: * the expected results. The test script format is compatible with existing
048: * scripts.
049: *
050: * @author ewanslater@users
051: * @author fredt@users
052: */
053: public class TestUtil {
054:
055: /**
056: * Runs a preformatted script.<p>
057: *
058: * Where a result set is required, each line in the script will
059: * be interpreted as a seperate expected row in the ResultSet
060: * returned by the query. Within each row, fields should be delimited
061: * using either comma (the default), or a user defined delimiter
062: * which should be specified in the System property TestUtilFieldDelimiter
063: * @param aConnection Connection object for the database
064: * @param aPath Path of the script file to be tested
065: */
066: static void testScript(Connection aConnection, String aPath) {
067:
068: try {
069: Statement statement = aConnection.createStatement();
070: File testfile = new File(aPath);
071: LineNumberReader reader = new LineNumberReader(
072: new FileReader(testfile));
073: HsqlArrayList section = null;
074:
075: print("Opened test script file: "
076: + testfile.getAbsolutePath());
077:
078: /**
079: * we read the lines from the start of one section of the script "/*"
080: * until the start of the next section, collecting the lines
081: * in the Vector lines.
082: * When a new section starts, we will pass the vector of lines
083: * to the test method to be processed.
084: */
085: int startLine = 1;
086:
087: while (true) {
088: boolean startSection = false;
089: String line = reader.readLine();
090:
091: if (line == null) {
092: break;
093: }
094:
095: line = line.substring(0, org.hsqldb.lib.StringUtil
096: .rTrimSize(line));
097:
098: //if the line is blank or a comment, then ignore it
099: if ((line.length() == 0) || line.startsWith("--")) {
100: continue;
101: }
102:
103: //...check if we're starting a new section...
104: if (line.startsWith("/*")) {
105: startSection = true;
106: }
107:
108: if (line.charAt(0) != ' ' && line.charAt(0) != '*') {
109: startSection = true;
110: }
111:
112: if (startSection) {
113:
114: //...if we are, test the previous section (if it exists)...
115: if (section != null) {
116: testSection(statement, section, startLine);
117: }
118:
119: //...and then start a new section...
120: section = new HsqlArrayList();
121: startLine = reader.getLineNumber();
122: }
123:
124: section.add(line);
125: }
126:
127: //send the last section for testing
128: if (section != null) {
129: testSection(statement, section, startLine);
130: }
131:
132: statement.close();
133: print("Processed lines: " + reader.getLineNumber());
134: } catch (Exception e) {
135: e.printStackTrace();
136: print("test script file error: " + e.getMessage());
137: }
138: }
139:
140: /**
141: * Performs a preformatted statement or group of statements and throws
142: * if the result does not match the expected one.
143: * @param line start line in the script file for this test
144: * @param stat Statement object used to access the database
145: * @param s Contains the type, expected result and SQL for the test
146: */
147: static void test(Statement stat, String s, int line) {
148:
149: //maintain the interface for this method
150: HsqlArrayList section = new HsqlArrayList();
151:
152: section.add(s);
153: testSection(stat, section, line);
154: }
155:
156: /**
157: * Method to save typing ;-)
158: * @param s String to be printed
159: */
160: static void print(String s) {
161: System.out.println(s);
162: }
163:
164: /**
165: * Takes a discrete section of the test script, contained in the
166: * section vector, splits this into the expected result(s) and
167: * submits the statement to the database, comparing the results
168: * returned with the expected results.
169: * If the actual result differs from that expected, or an
170: * exception is thrown, then the appropriate message is printed.
171: * @param stat Statement object used to access the database
172: * @param section Vector of script lines containing a discrete
173: * section of script (i.e. test type, expected results,
174: * SQL for the statement).
175: * @param line line of the script file where this section started
176: */
177: private static void testSection(Statement stat,
178: HsqlArrayList section, int line) {
179:
180: //create an appropriate instance of ParsedSection
181: ParsedSection pSection = parsedSectionFactory(section);
182:
183: if (pSection == null) { //it was not possible to sucessfully parse the section
184: print("The section starting at line " + line
185: + " could not be parsed, "
186: + "and so was not processed.\n");
187: } else if (pSection instanceof IgnoreParsedSection) {
188: print("Line " + line + ": " + pSection.getResultString());
189: } else if (pSection instanceof DisplaySection) {
190:
191: // May or may not want to report line number for 'd' sections. ?
192: print(pSection.getResultString());
193: } else if (!pSection.test(stat)) {
194: print("section starting at line " + line);
195: print("returned an unexpected result:");
196: print(pSection.toString());
197: }
198: }
199:
200: /**
201: * Factory method to create appropriate parsed section class for the section
202: * @param aSection Vector containing the section of script
203: * @return a ParesedSection object
204: */
205: private static ParsedSection parsedSectionFactory(
206: HsqlArrayList aSection) {
207:
208: //type of the section
209: char type = ' ';
210:
211: //section represented as an array of Strings, one for each significant
212: //line in the section
213: String[] rows = null;
214:
215: //read the first line of the Vector...
216: String topLine = (String) aSection.get(0);
217:
218: //...and check it for the type...
219: if (topLine.startsWith("/*")) {
220: type = topLine.charAt(2);
221:
222: //if the type code is invalid return null
223: if (!ParsedSection.isValidCode(type)) {
224: return null;
225: }
226:
227: //if the type code is UPPERCASE and system property IgnoreCodeCase
228: //has been set to true, make the type code lowercase
229: if ((Character.isUpperCase(type))
230: && (Boolean.getBoolean("IgnoreCodeCase"))) {
231: type = Character.toLowerCase(type);
232: }
233:
234: //...strip out the type declaration...
235: topLine = topLine.substring(3);
236: }
237:
238: //if, after stripping out the declaration from topLine, the length of topLine
239: //is greater than 0, then keep the rest of the line, as the first row.
240: //Otherwise it will be discarded, and the offset (between the array and the vector)
241: //set to 1.
242: int offset = 0;
243:
244: if (topLine.trim().length() > 0) {
245: rows = new String[aSection.size()];
246: rows[0] = topLine;
247: } else {
248: rows = new String[aSection.size() - 1];
249: offset = 1;
250: }
251:
252: //pull the rest of aSection into the rows array.
253: for (int i = (1 - offset); i < rows.length; i++) {
254: rows[i] = (String) aSection.get(i + offset);
255: }
256:
257: //then pass this to the constructor for the ParsedSection class that
258: //corresponds to the value of type
259: switch (type) {
260:
261: case 'u':
262: return new UpdateParsedSection(rows);
263:
264: case 's':
265: return new SilentParsedSection(rows);
266:
267: case 'r':
268: return new ResultSetParsedSection(rows);
269:
270: case 'c':
271: return new CountParsedSection(rows);
272:
273: case 'd':
274: return new DisplaySection(rows);
275:
276: case 'e':
277: return new ExceptionParsedSection(rows);
278:
279: case ' ':
280: return new BlankParsedSection(rows);
281:
282: default:
283:
284: //if we arrive here, then we should have a valid code,
285: //since we validated it earlier, so return an
286: //IgnoreParsedSection object
287: return new IgnoreParsedSection(rows, type);
288: }
289: }
290: }
291:
292: /**
293: * Abstract inner class representing a parsed section of script.
294: * The specific ParsedSections for each type of test should inherit from this.
295: */
296: abstract class ParsedSection {
297:
298: /**
299: * Type of this test.
300: * @see isValidCase() for allowed values
301: */
302: protected char type = ' ';
303:
304: /** error message for this section */
305: String message = null;
306:
307: /** contents of the section as an array of Strings, one for each line in the section. */
308: protected String[] lines = null;
309:
310: /** number of the last row containing results in sectionLines */
311: protected int resEndRow = 0;
312:
313: /** SQL query to be submitted to the database. */
314: protected String sqlString = null;
315:
316: /**
317: * Constructor when the section's input lines do not need to be parsed
318: * into SQL.
319: */
320: protected ParsedSection() {
321: }
322:
323: /**
324: * Common constructor functions for this family.
325: * @param aLines Array of the script lines containing the section of script.
326: * database
327: */
328: protected ParsedSection(String[] aLines) {
329:
330: lines = aLines;
331:
332: //read the lines array backwards to get out the SQL String
333: //using a StringBuffer for efficency until we've got the whole String
334: StringBuffer sqlBuff = new StringBuffer();
335: int endIndex = 0;
336: int k = lines.length - 1;
337:
338: do {
339:
340: //check to see if the row contains the end of the result set
341: if ((endIndex = lines[k].indexOf("*/")) != -1) {
342:
343: //then this is the end of the result set
344: sqlBuff.insert(0, lines[k].substring(endIndex + 2));
345:
346: lines[k] = lines[k].substring(0, endIndex);
347:
348: if (lines[k].length() == 0) {
349: resEndRow = k - 1;
350: } else {
351: resEndRow = k;
352: }
353:
354: break;
355: } else {
356: sqlBuff.insert(0, lines[k]);
357: }
358:
359: k--;
360: } while (k >= 0);
361:
362: //set sqlString value
363: sqlString = sqlBuff.toString();
364: }
365:
366: /**
367: * String representation of this ParsedSection
368: * @return String representation of this ParsedSection
369: */
370: public String toString() {
371:
372: StringBuffer b = new StringBuffer();
373:
374: b.append("\n******\n");
375: b.append("contents of lines array:\n");
376:
377: for (int i = 0; i < lines.length; i++) {
378: if (lines[i].trim().length() > 0) {
379: b.append("line ").append(i).append(": ").append(
380: lines[i]).append("\n");
381: }
382: }
383:
384: b.append("Type: ");
385: b.append(getType()).append("\n");
386: b.append("SQL: ").append(getSql()).append("\n");
387: b.append("results:\n");
388: b.append(getResultString());
389:
390: //check to see if the message field has been populated
391: if (getMessage() != null) {
392: b.append("\nmessage:\n");
393: b.append(getMessage());
394: }
395:
396: b.append("\n******\n");
397:
398: return b.toString();
399: }
400:
401: /**
402: * returns a String representation of the expected result for the test
403: * @return The expected result(s) for the test
404: */
405: protected abstract String getResultString();
406:
407: /**
408: * returns the error message for the section
409: *
410: * @return message
411: */
412: protected String getMessage() {
413: return message;
414: }
415:
416: /**
417: * returns the type of this section
418: * @return type of this section
419: */
420: protected char getType() {
421: return type;
422: }
423:
424: /**
425: * returns the SQL statement for this section
426: * @return SQL statement for this section
427: */
428: protected String getSql() {
429: return sqlString;
430: }
431:
432: /**
433: * performs the test contained in the section against the database.
434: * @param aStatement Statement object
435: * @return true if the result(s) are as expected, otherwise false
436: */
437: protected boolean test(Statement aStatement) {
438:
439: try {
440: aStatement.execute(getSql());
441: } catch (Exception x) {
442: message = x.getMessage();
443:
444: return false;
445: }
446:
447: return true;
448: }
449:
450: /**
451: * Checks that the type code letter is valid
452: * @param aCode type code to validate.
453: * @return true if the type code is valid, otherwise false.
454: */
455: protected static boolean isValidCode(char aCode) {
456:
457: /* Allowed values for test codes are:
458: * (note that UPPERCASE codes, while valid are only processed if the
459: * system property IgnoreCodeCase has been set to true)
460: *
461: * 'u' ('U') - update
462: * 'c' ('C') - count
463: * 'e' ('E') - exception
464: * 'r' ('R') - results
465: * 's' ('S') - silent
466: * 'd' - display (No reason to use upper-case).
467: * ' ' - not a test
468: */
469: char testChar = Character.toLowerCase(aCode);
470:
471: switch (testChar) {
472:
473: case ' ':
474: case 'r':
475: case 'e':
476: case 'c':
477: case 'u':
478: case 's':
479: case 'd':
480: return true;
481: }
482:
483: return false;
484: }
485: }
486:
487: /** Represents a ParsedSection for a ResultSet test */
488: class ResultSetParsedSection extends ParsedSection {
489:
490: private String delim = System.getProperty("TestUtilFieldDelimiter",
491: ",");
492: private String[] expectedRows = null;
493:
494: /**
495: * constructs a new instance of ResultSetParsedSection, interpreting
496: * the supplied results as one or more lines of delimited field values
497: * @param lines String[]
498: */
499: protected ResultSetParsedSection(String[] lines) {
500:
501: super (lines);
502:
503: type = 'r';
504:
505: //now we'll populate the expectedResults array
506: expectedRows = new String[(resEndRow + 1)];
507:
508: for (int i = 0; i <= resEndRow; i++) {
509: int skip = StringUtil.skipSpaces(lines[i], 0);
510:
511: expectedRows[i] = lines[i].substring(skip);
512: }
513: }
514:
515: protected String getResultString() {
516:
517: StringBuffer printVal = new StringBuffer();
518:
519: for (int i = 0; i < getExpectedRows().length; i++) {
520: printVal.append(getExpectedRows()[i]).append("\n");
521: }
522:
523: return printVal.toString();
524: }
525:
526: protected boolean test(Statement aStatement) {
527:
528: try {
529: try {
530:
531: //execute the SQL
532: aStatement.execute(getSql());
533: } catch (SQLException s) {
534: throw new Exception(
535: "Expected a ResultSet, but got the error: "
536: + s.getMessage());
537: }
538:
539: //check that update count != -1
540: if (aStatement.getUpdateCount() != -1) {
541: throw new Exception(
542: "Expected a ResultSet, but got an update count of "
543: + aStatement.getUpdateCount());
544: }
545:
546: //iterate over the ResultSet
547: ResultSet results = aStatement.getResultSet();
548: int count = 0;
549:
550: while (results.next()) {
551: if (count < getExpectedRows().length) {
552:
553: // String[] expectedFields = getExpectedRows()[count].split(delim);
554: String[] expectedFields = StringUtil.split(
555: getExpectedRows()[count], delim);
556:
557: //check that we have the number of columns expected...
558: if (results.getMetaData().getColumnCount() == expectedFields.length) {
559:
560: //...and if so, check that the column values are as expected...
561: int j = 0;
562:
563: for (int i = 0; i < expectedFields.length; i++) {
564: j = i + 1;
565:
566: String actual = results.getString(j);
567:
568: //...including null values...
569: if (actual == null) { //..then we have a null
570:
571: //...check to see if we were expecting it...
572: if (!expectedFields[i]
573: .equalsIgnoreCase("NULL")) {
574: throw new Exception(
575: "Expected row "
576: + count
577: + " of the ResultSet to contain:\n"
578: + getExpectedRows()[count]
579: + "\nbut field "
580: + j
581: + " contained NULL");
582: }
583: } else if (!actual
584: .equals(expectedFields[i])) {
585:
586: //then the results are different
587: throw new Exception(
588: "Expected row "
589: + (count + 1)
590: + " of the ResultSet to contain:\n"
591: + getExpectedRows()[count]
592: + "\nbut field " + j
593: + " contained "
594: + results.getString(j));
595: }
596: }
597: } else {
598:
599: //we have the wrong number of columns
600: throw new Exception(
601: "Expected the ResultSet to contain "
602: + expectedFields.length
603: + " fields, but it contained "
604: + results.getMetaData()
605: .getColumnCount()
606: + " fields.");
607: }
608: }
609:
610: count++;
611: }
612:
613: //check that we got as many rows as expected
614: if (count != getExpectedRows().length) {
615:
616: //we don't have the expected number of rows
617: throw new Exception(
618: "Expected the ResultSet to contain "
619: + getExpectedRows().length
620: + " rows, but it contained " + count
621: + " rows.");
622: }
623: } catch (Exception x) {
624: message = x.getMessage();
625:
626: return false;
627: }
628:
629: return true;
630: }
631:
632: private String[] getExpectedRows() {
633: return expectedRows;
634: }
635: }
636:
637: /** Represents a ParsedSection for an update test */
638: class UpdateParsedSection extends ParsedSection {
639:
640: //expected update count
641: int countWeWant;
642:
643: protected UpdateParsedSection(String[] lines) {
644:
645: super (lines);
646:
647: type = 'u';
648: countWeWant = Integer.parseInt(lines[0]);
649: }
650:
651: protected String getResultString() {
652: return Integer.toString(getCountWeWant());
653: }
654:
655: private int getCountWeWant() {
656: return countWeWant;
657: }
658:
659: protected boolean test(Statement aStatement) {
660:
661: try {
662: try {
663:
664: //execute the SQL
665: aStatement.execute(getSql());
666: } catch (SQLException s) {
667: throw new Exception("Expected an update count of "
668: + getCountWeWant() + ", but got the error: "
669: + s.getMessage());
670: }
671:
672: if (aStatement.getUpdateCount() != getCountWeWant()) {
673: throw new Exception("Expected an update count of "
674: + getCountWeWant()
675: + ", but got an update count of "
676: + aStatement.getUpdateCount() + ".");
677: }
678: } catch (Exception x) {
679: message = x.getMessage();
680:
681: return false;
682: }
683:
684: return true;
685: }
686: }
687:
688: /** Represents a ParsedSection for silent execution */
689: class SilentParsedSection extends ParsedSection {
690:
691: protected SilentParsedSection(String[] lines) {
692:
693: super (lines);
694:
695: type = 's';
696: }
697:
698: protected String getResultString() {
699: return null;
700: }
701:
702: protected boolean test(Statement aStatement) {
703:
704: try {
705: aStatement.execute(getSql());
706: } catch (Exception x) {
707: }
708:
709: return true;
710: }
711: }
712:
713: /** Represents a ParsedSection for a count test */
714: class CountParsedSection extends ParsedSection {
715:
716: //expected row count
717: private int countWeWant;
718:
719: protected CountParsedSection(String[] lines) {
720:
721: super (lines);
722:
723: type = 'c';
724: countWeWant = Integer.parseInt(lines[0]);
725: }
726:
727: protected String getResultString() {
728: return Integer.toString(getCountWeWant());
729: }
730:
731: private int getCountWeWant() {
732: return countWeWant;
733: }
734:
735: protected boolean test(Statement aStatement) {
736:
737: try {
738:
739: //execute the SQL
740: try {
741: aStatement.execute(getSql());
742: } catch (SQLException s) {
743: throw new Exception("Expected a ResultSet containing "
744: + getCountWeWant()
745: + " rows, but got the error: " + s.getMessage());
746: }
747:
748: //check that update count != -1
749: if (aStatement.getUpdateCount() != -1) {
750: throw new Exception(
751: "Expected a ResultSet, but got an update count of "
752: + aStatement.getUpdateCount());
753: }
754:
755: //iterate over the ResultSet
756: ResultSet results = aStatement.getResultSet();
757: int count = 0;
758:
759: while (results.next()) {
760: count++;
761: }
762:
763: //check that we got as many rows as expected
764: if (count != getCountWeWant()) {
765:
766: //we don't have the expected number of rows
767: throw new Exception(
768: "Expected the ResultSet to contain "
769: + getCountWeWant()
770: + " rows, but it contained " + count
771: + " rows.");
772: }
773: } catch (Exception x) {
774: message = x.getMessage();
775:
776: return false;
777: }
778:
779: return true;
780: }
781: }
782:
783: /** Represents a ParsedSection for an Exception test */
784: class ExceptionParsedSection extends ParsedSection {
785:
786: protected ExceptionParsedSection(String[] lines) {
787:
788: super (lines);
789:
790: type = 'e';
791: }
792:
793: protected String getResultString() {
794: return "SQLException";
795: }
796:
797: protected boolean test(Statement aStatement) {
798:
799: try {
800: aStatement.execute(getSql());
801: } catch (SQLException sqlX) {
802: return true;
803: } catch (Exception x) {
804: message = x.getMessage();
805:
806: return false;
807: }
808:
809: return false;
810: }
811: }
812:
813: /** Represents a ParsedSection for a section with blank type */
814: class BlankParsedSection extends ParsedSection {
815:
816: protected BlankParsedSection(String[] lines) {
817:
818: super (lines);
819:
820: type = ' ';
821: }
822:
823: protected String getResultString() {
824: return "No result specified for this section";
825: }
826: }
827:
828: /** Represents a ParsedSection that is to be ignored */
829: class IgnoreParsedSection extends ParsedSection {
830:
831: protected IgnoreParsedSection(String[] inLines, char aType) {
832:
833: /* Extremely ambiguous to use input parameter of same exact
834: * variable name as the superclass member "lines".
835: * Therefore, renaming to inLines. */
836:
837: // Inefficient to parse this into SQL when we aren't going to use
838: // it as SQL. Should probably just be removed to use the
839: // super() constructor.
840: super (inLines);
841:
842: type = aType;
843: }
844:
845: protected String getResultString() {
846: return "This section, of type '" + getType() + "' was ignored";
847: }
848: }
849:
850: /** Represents a Section to be Displayed, not executed */
851: class DisplaySection extends ParsedSection {
852:
853: protected DisplaySection(String[] inLines) {
854:
855: /* Can't user the super constructor, since it does funny things when
856: * constructing the SQL Buffer, which we don't need. */
857: lines = inLines;
858:
859: int firstSlash = lines[0].indexOf('/');
860:
861: lines[0] = lines[0].substring(firstSlash + 1);
862: }
863:
864: protected String getResultString() {
865:
866: StringBuffer sb = new StringBuffer();
867:
868: for (int i = 0; i < lines.length; i++) {
869: if (i > 0) {
870: sb.append('\n');
871: }
872:
873: sb.append("+ " + lines[i]);
874: }
875:
876: return sb.toString();
877: }
878: }
|