1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17
18package tests.java.sql;
19
20import SQLite.Database;
21import SQLite.Function;
22import SQLite.FunctionContext;
23import dalvik.annotation.TestTargets;
24import dalvik.annotation.TestLevel;
25import dalvik.annotation.TestTargetNew;
26import dalvik.annotation.TestTargetClass;
27
28import junit.extensions.TestSetup;
29import junit.framework.Test;
30import junit.framework.TestCase;
31import junit.framework.TestSuite;
32
33import tests.support.Support_SQL;
34
35import java.sql.CallableStatement;
36import java.sql.Connection;
37import java.sql.PreparedStatement;
38import java.sql.ResultSet;
39import java.sql.SQLException;
40import java.sql.Statement;
41import java.util.Collection;
42import java.util.Collections;
43import java.util.HashSet;
44
45
46/**
47 * Functional test for the Statement.setQueryTimeout() method. Adopted from
48 * Apache Derby project (Apache License 2.0).
49 *
50 * TODO Test requires transaction isolation to be supported. => Ticket 69
51 *
52 * This test consists of four parts: 1. Executes a SELECT
53 * query in 4 different threads concurrently. The query calls a user-defined,
54 * server-side function which delays the execution, so that it takes several
55 * seconds even though the data volume is really low. The fetch operations take
56 * longer time than the timeout value set. Hence, this part tests getting
57 * timeouts from calls to ResultSet.next(). Two connections are used, two
58 * threads execute their statement in the context of one connection, the other
59 * two threads in the context of the other connection. Of the 4 threads, only
60 * one executes its statement with a timeout value. This way, the test ensures
61 * that the correct statement is affected by setQueryTimeout(), regardless of
62 * what connection/transaction it and other statements are executed in the
63 * context of.
64 *
65 * 2. Executes an INSERT query in multiple threads. This part tests
66 * getting timeouts from calls to Statement.execute(). Each thread executes the
67 * query in the context of a separate connection. There is no point in executing
68 * multiple statements on the same connection; since only one statement per
69 * connection executes at a time, there will be no interleaving of execution
70 * between them (contrary to the first part of this test, where calls to
71 * ResultSet.next() may be interleaved between the different threads). Half of
72 * the threads execute their statement with a timeout value set, this is to
73 * verify that the correct statements are affected by the timeout, while the
74 * other statements execute to completion.
75 * 3. Sets an invalid (negative)
76 * timeout. Verifies that the correct exception is thrown.
77 * 4. Tests that the query timeout value is not forgotten after the execution of a statement.
78 */
79@TestTargetClass(Statement.class)
80public class QueryTimeoutTest extends TestCase {
81
82    private static Statement statement;
83
84    private static final int TIMEOUT = 1; // In seconds
85    private static final int CONNECTIONS = 100;
86
87    private static Connection[] connections = new Connection[CONNECTIONS];
88
89    private static void printSQLException(SQLException e) {
90        while (e != null) {
91            e.printStackTrace();
92            e = e.getNextException();
93        }
94    }
95
96    /**
97     * This Exception class is used for getting fail-fast behaviour in this
98     * test. There is no point in wasting cycles running a test to the end when
99     * we know that it has failed. In order to enable chaining of exceptions in
100     * J2ME, this class defines its own "cause", duplicating existing
101     * functionality in J2SE.
102     */
103    private static class TestFailedException extends Exception {
104        private Throwable cause;
105
106        public TestFailedException(Throwable t) {
107            super();
108            cause = t;
109        }
110
111        public TestFailedException(String message) {
112            super(message);
113            cause = null;
114        }
115
116        public TestFailedException(String message, Throwable t) {
117            super(message);
118            cause = t;
119        }
120
121        public String toString() {
122            if (cause != null) {
123                return super.toString() + ": " + cause.toString();
124            } else {
125                return super.toString();
126            }
127        }
128
129        public void printStackTrace() {
130            super.printStackTrace();
131            if (cause != null) {
132                if (cause instanceof SQLException) {
133                    QueryTimeoutTest.printSQLException((SQLException) cause);
134                } else {
135                    cause.printStackTrace();
136                }
137            }
138        }
139    }
140
141    /**
142     * Used for executing the SQL statements for setting up this test (the
143     * preparation phase). The queries testing setQueryTimeout() are run by the
144     * StatementExecutor class.
145     */
146    private static void exec(Connection connection, String queryString,
147            Collection ignoreExceptions) throws TestFailedException {
148        Statement statement = null;
149        try {
150            statement = connection.createStatement();
151            System.out.println(" Executing "+queryString);
152            statement.execute(queryString);
153        } catch (SQLException e) {
154            String sqlState = e.getSQLState();
155            if (!ignoreExceptions.contains(sqlState)) {
156                throw new TestFailedException(e); // See finally block below
157            }
158        } finally {
159            if (statement != null) {
160                try {
161                    statement.close();
162                } catch (SQLException ee) {
163                    // This will discard an exception possibly thrown above :-(
164                    // But we don't worry too much about this, since:
165                    // 1. This is just a test
166                    // 2. We don't expect close() to throw
167                    // 3. If it does, this will be inspected by a developer
168                    throw new TestFailedException(ee);
169                }
170            }
171        }
172    }
173
174    // Convenience method
175    private static void exec(Connection connection, String queryString)
176            throws TestFailedException {
177        exec(connection, queryString, Collections.EMPTY_SET);
178    }
179
180    private static void dropTables(Connection conn, String tablePrefix)
181            throws TestFailedException {
182        Collection ignore = new HashSet();
183        //ignore.add("42Y55");
184
185        exec(conn, "drop table if exists " + tablePrefix + "_orig;", ignore);
186        exec(conn, "drop table if exists " + tablePrefix + "_copy;", ignore);
187    }
188
189    private static void prepareTables(Connection conn, String tablePrefix)
190            throws TestFailedException {
191        System.out.println("Initializing tables with prefix " + tablePrefix);
192
193        dropTables(conn, tablePrefix);
194
195        exec(conn, "create table " + tablePrefix + "_orig (a int)");
196
197        exec(conn, "create table " + tablePrefix + "_copy (a int)");
198
199        for (int i = 0; i < 7; i++) {
200        exec(conn, "insert into " + tablePrefix + "_orig"
201                + " values ("+i+");");
202        }
203    }
204
205    private static String getFetchQuery(String tablePrefix) {
206        /**
207         * The reason for using the mod function here is to force at least one
208         * invocation of ResultSet.next() to read more than one row from the
209         * table before returning. This is necessary since timeout is checked
210         * only when reading rows from base tables, and when the first row is
211         * read, the query still has not exceeded the timeout.
212         */
213        return "select a from " + tablePrefix
214                + "_orig where mod(DELAY(1,a),3)=0";
215    }
216
217    private static String getExecQuery(String tablePrefix) {
218        return "insert into " + tablePrefix + "_copy select a from "
219                + tablePrefix + "_orig where DELAY(1,1)=1";
220    }
221
222    private static class StatementExecutor extends Thread {
223        private PreparedStatement statement;
224        private boolean doFetch;
225        private int timeout;
226        private SQLException sqlException;
227        private String name;
228        private long highestRunTime;
229
230        public StatementExecutor(PreparedStatement statement, boolean doFetch,
231                int timeout) {
232            this.statement = statement;
233            this.doFetch = doFetch;
234            this.timeout = timeout;
235            highestRunTime = 0;
236            sqlException = null;
237            if (timeout > 0) {
238                try {
239                    statement.setQueryTimeout(timeout);
240                } catch (SQLException e) {
241                    sqlException = e;
242                }
243            }
244        }
245
246        private void setHighestRunTime(long runTime) {
247            synchronized (this) {
248                highestRunTime = runTime;
249            }
250        }
251
252        public long getHighestRunTime() {
253            synchronized (this) {
254                return highestRunTime;
255            }
256        }
257
258        private boolean fetchRow(ResultSet resultSet) throws SQLException {
259            long startTime = System.currentTimeMillis();
260            boolean hasNext = resultSet.next();
261            long endTime = System.currentTimeMillis();
262            long runTime = endTime - startTime;
263            if (runTime > highestRunTime) setHighestRunTime(runTime);
264            return hasNext;
265        }
266
267        public void run() {
268            if (sqlException != null) return;
269
270            ResultSet resultSet = null;
271
272            try {
273                if (doFetch) {
274                    long startTime = System.currentTimeMillis();
275                    resultSet = statement.executeQuery();
276                    long endTime = System.currentTimeMillis();
277                    setHighestRunTime(endTime - startTime);
278                    while (fetchRow(resultSet)) {
279                        yield();
280                    }
281                } else {
282                    long startTime = System.currentTimeMillis();
283                    statement.execute();
284                    long endTime = System.currentTimeMillis();
285                    setHighestRunTime(endTime - startTime);
286                }
287            } catch (SQLException e) {
288                synchronized (this) {
289                    sqlException = e;
290                }
291            } finally {
292                if (resultSet != null) {
293                    try {
294                        resultSet.close();
295                    } catch (SQLException ex) {
296                        if (sqlException != null) {
297                            System.err.println("Discarding previous exception");
298                            sqlException.printStackTrace();
299                        }
300                        sqlException = ex;
301                    }
302                }
303            }
304        }
305
306        public SQLException getSQLException() {
307            synchronized (this) {
308                return sqlException;
309            }
310        }
311    }
312
313    /**
314     * This method compares a thrown SQLException's SQLState value to an
315     * expected SQLState. If they do not match, a TestFailedException is thrown
316     * with the given message string.
317     */
318    private static void expectException(String expectSqlState,
319            SQLException sqlException, String failMsg)
320            throws TestFailedException {
321        if (sqlException == null) {
322            throw new TestFailedException(failMsg);
323        } else {
324            String sqlState = sqlException.getSQLState();
325            if (!expectSqlState.equals(sqlState)) {
326                throw new TestFailedException(sqlException);
327            }
328        }
329    }
330
331    // A convenience method which wraps a SQLException
332    private static PreparedStatement prepare(Connection conn, String query)
333            throws TestFailedException {
334        try {
335            return conn.prepareStatement(query);
336        } catch (SQLException e) {
337            throw new TestFailedException(e);
338        }
339    }
340
341    /**
342     * Part 1 of this test.
343     */
344    @TestTargetNew(
345        level = TestLevel.PARTIAL_COMPLETE,
346        notes = "Testing timeout with fetch operations",
347        method = "setQueryTimeout",
348        args = {int.class}
349    )
350    public static void testTimeoutWithFetch() throws TestFailedException {
351        System.out.println("Testing timeout with fetch operations");
352
353        Connection conn1 = connections[0];
354        Connection conn2 = connections[1];
355
356        try {
357            conn1.setAutoCommit(false);
358            conn2.setAutoCommit(false);
359        } catch (SQLException e) {
360            throw new TestFailedException("Unexpected Exception", e);
361        }
362
363        // The idea with these 4 statements is as follows:
364        // A - should time out
365        // B - different stmt on the same connection; should NOT time out
366        // C - different stmt on different connection; should NOT time out
367        // D - here just to create equal contention on conn1 and conn2
368
369        PreparedStatement statementA = prepare(conn1, getFetchQuery("t"));
370        PreparedStatement statementB = prepare(conn1, getFetchQuery("t"));
371        PreparedStatement statementC = prepare(conn2, getFetchQuery("t"));
372        PreparedStatement statementD = prepare(conn2, getFetchQuery("t"));
373
374        StatementExecutor[] statementExecutor = new StatementExecutor[4];
375        statementExecutor[0] = new StatementExecutor(statementA, true, TIMEOUT);
376        statementExecutor[1] = new StatementExecutor(statementB, true, 0);
377        statementExecutor[2] = new StatementExecutor(statementC, true, 0);
378        statementExecutor[3] = new StatementExecutor(statementD, true, 0);
379
380        for (int i = 3; i >= 0; --i) {
381            statementExecutor[i].start();
382        }
383
384        for (int i = 0; i < 4; ++i) {
385            try {
386                statementExecutor[i].join();
387            } catch (InterruptedException e) {
388                throw new TestFailedException("Should never happen", e);
389            }
390        }
391
392        /**
393         * Actually, there is no guarantee that setting a query timeout for a
394         * statement will actually cause a timeout, even if execution of the
395         * statement takes longer than the specified timeout. However, these
396         * queries execute significantly longer than the specified query
397         * timeout. Also, the cancellation mechanism implemented should be quite
398         * responsive. In sum, we expect the statement to always time out. If it
399         * does not time out, however, we print the highest execution time for
400         * the query, as an assistance in determining why it failed. Compare the
401         * number to the TIMEOUT constant in this class (note that the TIMEOUT
402         * constant is in seconds, while the execution time is in milliseconds).
403         */
404        expectException("XCL52", statementExecutor[0].getSQLException(),
405                "fetch did not time out. Highest execution time: "
406                        + statementExecutor[0].getHighestRunTime() + " ms");
407
408        System.out.println("Statement 0 timed out");
409
410        for (int i = 1; i < 4; ++i) {
411            SQLException sqlException = statementExecutor[i].getSQLException();
412            if (sqlException != null) {
413                throw new TestFailedException("Unexpected exception in " + i,
414                        sqlException);
415            }
416            System.out.println("Statement " + i + " completed");
417        }
418
419        try {
420            statementA.close();
421            statementB.close();
422            statementC.close();
423            statementD.close();
424            conn1.commit();
425            conn2.commit();
426        } catch (SQLException e) {
427            throw new TestFailedException(e);
428        }
429    }
430
431    /**
432     *
433     * @test {@link java.sql.Statement#setQueryTimeout(int) }
434     *
435     * Part two of this test.
436     */
437    @TestTargetNew(
438        level = TestLevel.PARTIAL_COMPLETE,
439        notes = "test timeout with st.exec()",
440        method = "setQueryTimeout",
441        args = {int.class}
442    )
443    public static void testTimeoutWithExec()
444            throws TestFailedException {
445        System.out.println("Testing timeout with an execute operation");
446
447        for (int i = 0; i < connections.length; ++i) {
448            try {
449                connections[i].setAutoCommit(true);
450            } catch (SQLException e) {
451                throw new TestFailedException("Unexpected Exception", e);
452            }
453        }
454
455        PreparedStatement statements[] = new PreparedStatement[connections.length];
456        for (int i = 0; i < statements.length; ++i) {
457            statements[i] = prepare(connections[i], getExecQuery("t"));
458        }
459
460        StatementExecutor[] executors = new StatementExecutor[statements.length];
461        for (int i = 0; i < executors.length; ++i) {
462            int timeout = (i % 2 == 0) ? TIMEOUT : 0;
463            executors[i] = new StatementExecutor(statements[i], false, timeout);
464        }
465
466        for (int i = 0; i < executors.length; ++i) {
467            executors[i].start();
468        }
469
470        for (int i = 0; i < executors.length; ++i) {
471            try {
472                executors[i].join();
473            } catch (InterruptedException e) {
474                throw new TestFailedException("Should never happen", e);
475            }
476        }
477
478        /**
479         * Actually, there is no guarantee that setting a query timeout for a
480         * statement will actually cause a timeout, even if execution of the
481         * statement takes longer than the specified timeout. However, these
482         * queries execute significantly longer than the specified query
483         * timeout. Also, the cancellation mechanism implemented should be quite
484         * responsive. In sum, we expect the statement to always time out. If it
485         * does not time out, however, we print the highest execution time for
486         * the query, as an assistance in determining why it failed. Compare the
487         * number to the TIMEOUT constant in this class (note that the TIMEOUT
488         * constant is in seconds, while the execution time is in milliseconds).
489         */
490        for (int i = 0; i < executors.length; ++i) {
491            int timeout = (i % 2 == 0) ? TIMEOUT : 0;
492            if (timeout > 0) {
493                expectException("XCL52", executors[i].getSQLException(),
494                        "exec did not time out. Execution time: "
495                                + executors[i].getHighestRunTime() + " ms");
496            } else {
497                SQLException sqlException = executors[i].getSQLException();
498                if (sqlException != null) {
499                    throw new TestFailedException(sqlException);
500                }
501            }
502        }
503
504        System.out
505                .println("Statements that should time out timed out, and statements that should complete completed");
506
507        for (int i = 0; i < statements.length; ++i) {
508            try {
509                statements[i].close();
510            } catch (SQLException e) {
511                throw new TestFailedException(e);
512            }
513        }
514    }
515
516
517    /**
518     *
519     * @test {@link java.sql.Statement#setQueryTimeout(int) }
520     *
521     */
522    @TestTargetNew(
523        level = TestLevel.PARTIAL_COMPLETE,
524        notes = "Testing setting a negative timeout value",
525        method = "setQueryTimeout",
526        args = {int.class}
527    )
528    public static void testInvalidTimeoutValue(Connection conn)
529            throws TestFailedException {
530
531        try {
532            conn.setAutoCommit(true);
533        } catch (SQLException e) {
534            throw new TestFailedException("Unexpected Exception", e);
535        }
536
537        // Create statement
538        PreparedStatement stmt = null;
539        try {
540            stmt = conn.prepareStatement("select * from sys.systables");
541        } catch (SQLException e) {
542            throw new TestFailedException("Unexpected Exception", e);
543        }
544
545        // Set (invalid) timeout value - expect exception
546        try {
547            stmt.setQueryTimeout(-1);
548        } catch (SQLException e) {
549            expectException("XJ074", e,
550                    "negative timeout value should give exception");
551        }
552
553        System.out
554                .println("Negative timeout value caused exception, as expected");
555
556        // Execute the statement and fetch result
557        ResultSet rs = null;
558        try {
559            rs = stmt.executeQuery();
560            System.out.println("Execute returned a ResultSet");
561            rs.close();
562        } catch (SQLException e) {
563            throw new TestFailedException("Unexpected Exception", e);
564        } finally {
565            try {
566                stmt.close();
567            } catch (SQLException e) {
568                // This will discard an exception possibly thrown above :-(
569                // But we don't worry too much about this, since:
570                // 1. This is just a test
571                // 2. We don't expect close() to throw
572                // 3. If it does, this will be inspected by a developer
573                throw new TestFailedException("close should not throw", e);
574            }
575        }
576    }
577
578    /**
579     *
580     * @test {@link java.sql.Statement#setQueryTimeout(int) }
581     *
582     * Part two of this test.
583     */
584    @TestTargetNew(
585        level = TestLevel.PARTIAL_COMPLETE,
586        notes = "timeout with executeUpdate call",
587        method = "setQueryTimeout",
588        args = {int.class}
589    )
590    public static void testTimeoutWithExecuteUpdate()
591            throws TestFailedException {
592        System.out.println("Testing timeout with executeUpdate call.");
593        try {
594            Statement stmt = connections[0].createStatement();
595            stmt.setQueryTimeout(TIMEOUT);
596            stmt.executeUpdate(getExecQuery("t"));
597        } catch (SQLException sqle) {
598            expectException("XCL52", sqle, "Should have timed out.");
599        }
600    }
601
602    /** Test for DERBY-1692. */
603    @TestTargetNew(
604            level = TestLevel.PARTIAL_COMPLETE,
605            notes = "Testing that Statement considers timeout.",
606            method = "setQueryTimeout",
607            args = {int.class}
608        )
609    public static void testRememberTimeoutValue()
610            throws TestFailedException {
611        String sql = getFetchQuery("t");
612        try {
613            Statement stmt = connections[0].createStatement();
614            statementRemembersTimeout(stmt);
615            PreparedStatement ps = connections[0].prepareStatement(sql);
616            statementRemembersTimeout(ps);
617            CallableStatement cs = connections[0].prepareCall(sql);
618            statementRemembersTimeout(cs);
619        } catch (SQLException sqle) {
620            throw new TestFailedException("Unexpected Exception", sqle);
621        }
622    }
623
624    public static void statementRemembersTimeout(Statement stmt)
625            throws SQLException, TestFailedException {
626        System.out.println("Testing that Statement remembers timeout.");
627        stmt.setQueryTimeout(1);
628        for (int i = 0; i < 3; i++) {
629            try {
630                ResultSet rs = stmt.executeQuery(getFetchQuery("t"));
631                while (rs.next()) {
632                    // do nothing
633                }
634                throw new TestFailedException("Should have timed out.");
635            } catch (SQLException sqle) {
636                expectException("XCL52", sqle, "Should have timed out.");
637            }
638        }
639        stmt.close();
640    }
641
642    private static void statementRemembersTimeout(PreparedStatement ps)
643            throws SQLException, TestFailedException {
644        String name = (ps instanceof CallableStatement) ? "CallableStatement"
645                : "PreparedStatement";
646        System.out.println("Testing that " + name + " remembers timeout.");
647        ps.setQueryTimeout(1);
648        for (int i = 0; i < 3; i++) {
649            try {
650                ResultSet rs = ps.executeQuery();
651                while (rs.next()) {
652                    // do nothing
653                }
654                throw new TestFailedException("Should have timed out.");
655            } catch (SQLException sqle) {
656                expectException("XCL52", sqle, "Should have timed out.");
657            }
658        }
659        ps.close();
660    }
661
662    /**
663     * A function
664     * arg0 : int seconds
665     *
666     */
667    static class Delay implements SQLite.Function {
668
669        public void function(FunctionContext fc, String[] args) {
670            int seconds = new Integer(args[0]).intValue();
671            int value = new Integer(args[1]).intValue();
672            try {
673                Thread.sleep(seconds * 1000);
674            } catch (InterruptedException e) {
675                // Ignore
676            }
677            fc.set_result(value);
678
679        }
680
681        public void last_step(FunctionContext fc) {
682            // TODO Auto-generated method stub
683
684        }
685
686        public void step(FunctionContext fc, String[] args) {
687            // TODO Auto-generated method stub
688
689        }
690
691    }
692
693    /**
694     * The actual main bulk of this test. Sets up the environment, prepares
695     * tables, runs the tests, and shuts down.
696     */
697    public static Test suite() {
698
699        TestSetup setup = new TestSetup( new TestSuite (QueryTimeoutTest.class)) {
700            public void setUp() {
701
702                // Establish connections
703                Support_SQL.loadDriver();
704                try {
705
706                    for (int i = 0; i < connections.length; ++i) {
707                        connections[i] = Support_SQL.getConnection();
708                    }
709
710                    for (int i = 0; i < connections.length; ++i) {
711                        connections[i]
712                                .setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
713                    }
714
715                    // setup Delay function
716                    prepare();
717
718                } catch (Throwable e) {
719                    fail("Unexpected SQLException " + e.toString());
720                }
721
722                System.out.println("Connections set up");
723
724            }
725
726            public void tearDown() {
727                for (int i = connections.length - 1; i >= 0; --i) {
728                    if (connections[i] != null) {
729                        try {
730                            connections[i].close();
731                        } catch (SQLException ex) {
732                            printSQLException(ex);
733                        }
734                    }
735                }
736                System.out.println("Closed connections");
737            }
738
739            public void prepare() throws TestFailedException {
740                System.out.println("Preparing for testing queries with timeout");
741                Database db = new Database();
742
743                Connection conn = connections[0];
744
745
746                try {
747                    db.open(Support_SQL.getFilename(), 1);
748                    conn.setAutoCommit(true);
749                } catch (Exception e) {
750                    throw new TestFailedException("Unexpected Exception", e);
751                }
752
753                Function delayFc = new Delay();
754                db.create_function("DELAY", 2, delayFc);
755
756                prepareTables(conn, "t");
757            }
758        };
759
760        TestSuite ts = new TestSuite();
761        ts.addTestSuite(QueryTimeoutTest.class);
762
763        return setup;
764    }
765
766}
767