diff --git a/mondrian/src/test/java/mondrian/junit5/DataLoader.java b/mondrian/src/test/java/mondrian/junit5/DataLoader.java new file mode 100644 index 0000000000..a54fc3b992 --- /dev/null +++ b/mondrian/src/test/java/mondrian/junit5/DataLoader.java @@ -0,0 +1,10 @@ +package mondrian.junit5; + +public interface DataLoader { + /** + * @param jdbcConnectionUrl - jdbcConnectionUrl + * @return jdbc connection String + */ + boolean loadDataData(String jdbcConnectionUrl); + +} diff --git a/mondrian/src/test/java/mondrian/junit5/DatabaseHandler.java b/mondrian/src/test/java/mondrian/junit5/DatabaseHandler.java new file mode 100644 index 0000000000..e39c972a60 --- /dev/null +++ b/mondrian/src/test/java/mondrian/junit5/DatabaseHandler.java @@ -0,0 +1,15 @@ +package mondrian.junit5; + +import java.io.Closeable; +import java.util.Map; + +public interface DatabaseHandler extends Closeable { + + /** + * + * @param props - properties + * @return jdbc connection String + */ + String setUpDatabase(Map props); + +} diff --git a/mondrian/src/test/java/mondrian/junit5/FoodmardDataLoader.java b/mondrian/src/test/java/mondrian/junit5/FoodmardDataLoader.java new file mode 100644 index 0000000000..8c7821f5ab --- /dev/null +++ b/mondrian/src/test/java/mondrian/junit5/FoodmardDataLoader.java @@ -0,0 +1,24 @@ +package mondrian.junit5; + +import mondrian.test.loader.MondrianFoodMartLoaderX; + +public class FoodmardDataLoader implements DataLoader{ + + @Override + public boolean loadDataData(String jdbcUrl) { + String[] args=new String[]{ + "-verbose", + "-tables", + "-data", + "-indexes", + "-outputJdbcURL="+jdbcUrl, +// "-outputJdbcUser="+mySQLContainer.getUsername(), +// "-outputJdbcPassword="+mySQLContainer.getPassword(), + "-outputJdbcBatchSize=50", + "-jdbcDrivers=com.mysql.cl.jdbc.Driver" + }; + MondrianFoodMartLoaderX.main(args); + return true; + } + +} diff --git a/mondrian/src/test/java/mondrian/junit5/MondrianRuntimeExtension.java b/mondrian/src/test/java/mondrian/junit5/MondrianRuntimeExtension.java index b92bee2d6f..a7e68632fe 100644 --- a/mondrian/src/test/java/mondrian/junit5/MondrianRuntimeExtension.java +++ b/mondrian/src/test/java/mondrian/junit5/MondrianRuntimeExtension.java @@ -1,31 +1,23 @@ package mondrian.junit5; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.extension.ExtensionContext.Namespace.GLOBAL; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; -import java.util.function.Consumer; +import java.util.Locale; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtensionContext; import org.testcontainers.DockerClientFactory; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.containers.output.OutputFrame; -import mondrian.test.loader.MondrianFoodMartLoaderX; +import mondrian.resource.MondrianResource; public class MondrianRuntimeExtension implements ExecutionCondition, BeforeAllCallback, ExtensionContext.Store.CloseableResource { - static Consumer x = t -> System.out.println(t.getUtf8String()); + private static boolean started = false; - private static MySQLContainer mySQLContainer; @Override public void beforeAll(ExtensionContext context) { @@ -34,65 +26,21 @@ public void beforeAll(ExtensionContext context) { started = true; // registers a callback hook when the root test context is shut down context.getRoot().getStore(GLOBAL).put("MondrianRuntimeExtensionClosableCallbackHook", this); + defineLocale(); - try { - initDB(); - loadFootMart(); - } catch (SQLException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } System.out.println("##############################################################"); } } - private void loadFootMart() { - - String[] args=new String[]{ - "-verbose", - "-tables", - "-data", - "-indexes", - "-outputJdbcURL="+mySQLContainer.getJdbcUrl(), - "-outputJdbcUser="+mySQLContainer.getUsername(), - "-outputJdbcPassword="+mySQLContainer.getPassword(), - "-outputJdbcBatchSize=50", - "-jdbcDrivers=com.mysql.cl.jdbc.Driver" - }; - MondrianFoodMartLoaderX.main(args); - + private void defineLocale() { + MondrianResource.setThreadLocale( Locale.US ); } - private void initDB() throws SQLException { - - mySQLContainer = new MySQLContainer<>("mysql:5.7.34").withDatabaseName("TEST") - .withUsername("user") - .withPassword("pass") - .withEnv("MYSQL_ROOT_HOST", "%").withLogConsumer(x); - System.out.println("11##############################################################"); - - mySQLContainer.start(); - System.out.println("21##############################################################"); - - String url = mySQLContainer.getJdbcUrl(); - System.out.println(url); - Connection con = DriverManager.getConnection(url, "user", "pass"); -// Statement stmt = con.createStatement(); -// ResultSet rs = stmt.executeQuery("SELECT version() "); -// rs.next(); -// String resultSetString = rs.getString(1); - - //assertTrue(resultSetString.startsWith("8"), "The database version can be set using a container rule parameter"); - - } @Override public void close() { // Your "after all tests" logic goes here - if(mySQLContainer!=null) { - mySQLContainer.stop(); - mySQLContainer.close(); - } + } @Override diff --git a/mondrian/src/test/java/mondrian/junit5/MondrianRuntimeSupport.java b/mondrian/src/test/java/mondrian/junit5/MondrianRuntimeSupport.java index b63c41ea80..a774b814d7 100644 --- a/mondrian/src/test/java/mondrian/junit5/MondrianRuntimeSupport.java +++ b/mondrian/src/test/java/mondrian/junit5/MondrianRuntimeSupport.java @@ -17,5 +17,7 @@ @Documented @ExtendWith(MondrianRuntimeExtension.class) public @interface MondrianRuntimeSupport { - + Class database() default MySQLDatabaseHandler.class; + Class dataLoader() default FoodmardDataLoader.class; + } diff --git a/mondrian/src/test/java/mondrian/junit5/MySQLDatabaseHandler.java b/mondrian/src/test/java/mondrian/junit5/MySQLDatabaseHandler.java new file mode 100644 index 0000000000..7600f03f2a --- /dev/null +++ b/mondrian/src/test/java/mondrian/junit5/MySQLDatabaseHandler.java @@ -0,0 +1,45 @@ +package mondrian.junit5; + +import java.io.IOException; +import java.sql.DriverManager; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.output.OutputFrame; + +public class MySQLDatabaseHandler implements DatabaseHandler { + + private static MySQLContainer mySQLContainer; + static Consumer x = t -> System.out.println(t.getUtf8String()); + + @Override + public void close() throws IOException { + if (mySQLContainer != null) { + mySQLContainer.stop(); + mySQLContainer.close(); + } + } + + @Override + public String setUpDatabase(Map props) { + String user=props.getOrDefault("username", UUID.randomUUID().toString().replace("-","")).toString(); + String pass=props.getOrDefault("password", UUID.randomUUID().toString().replace("-","")).toString(); + + + mySQLContainer = new MySQLContainer<>("mysql:5.7.34").withDatabaseName(user).withUsername(pass) + .withPassword("pass").withEnv("MYSQL_ROOT_HOST", "%").withLogConsumer(x); + + mySQLContainer.start(); + + return mySQLContainer.getJdbcUrl()+"?user="+user+"&password="+pass; +// Connection con = DriverManager.getConnection(url, "user", "pass"); + + } + + + + + +} diff --git a/mondrian/src/test/java/mondrian/spi/impl/OracleDialectTest.java b/mondrian/src/test/java/mondrian/spi/impl/OracleDialectTest.java index dec8c754d1..b98c786b08 100644 --- a/mondrian/src/test/java/mondrian/spi/impl/OracleDialectTest.java +++ b/mondrian/src/test/java/mondrian/spi/impl/OracleDialectTest.java @@ -31,7 +31,7 @@ public class OracleDialectTest{ private OracleDialect dialect; @BeforeEach - protected void setUp() throws Exception { + public void setUp() throws Exception { when( metaData.getDatabaseProductName() ).thenReturn( Dialect.DatabaseProduct.ORACLE.name() ); when( connection.getMetaData() ).thenReturn( metaData ); dialect = new OracleDialect( connection ); diff --git a/mondrian/src/test/java/mondrian/test/DelegatingTestContext.java b/mondrian/src/test/java/mondrian/test/DelegatingTestContext.java index 5d5a8fc137..589e8d6214 100644 --- a/mondrian/src/test/java/mondrian/test/DelegatingTestContext.java +++ b/mondrian/src/test/java/mondrian/test/DelegatingTestContext.java @@ -14,7 +14,7 @@ import java.io.PrintWriter; /** - * Extension of {@link TestContext} which delegates all behavior to + * Extension of {@link FoodmartTestContextImpl} which delegates all behavior to * a parent test context. * *

Derived classes can selectively override methods. @@ -22,7 +22,7 @@ * @author jhyde * @since 7 September, 2005 */ -public class DelegatingTestContext extends TestContext { +public class DelegatingTestContext extends FoodmartTestContextImpl { protected final TestContext context; protected DelegatingTestContext(TestContext context) { diff --git a/mondrian/src/test/java/mondrian/test/FoodMartTestCase.java b/mondrian/src/test/java/mondrian/test/FoodMartTestCase.java index 3b42e4c695..06c3fa04a8 100644 --- a/mondrian/src/test/java/mondrian/test/FoodMartTestCase.java +++ b/mondrian/src/test/java/mondrian/test/FoodMartTestCase.java @@ -46,8 +46,8 @@ public class FoodMartTestCase { * Returns the test context. Override this method if you wish to use a * different source for your FoodMart connection. */ - public static TestContext getTestContext() { - return TestContext.instance(); + public static FoodmartTestContextImpl getTestContext() { + return FoodmartTestContextImpl.instance(); } protected static Connection getConnection() { @@ -176,9 +176,9 @@ protected static void assertQueriesReturnSimilarResults( TestContext testContext) { String resultString1 = - TestContext.toString(testContext.executeQuery(query1)); + FoodmartTestContextImpl.toString(testContext.executeQuery(query1)); String resultString2 = - TestContext.toString(testContext.executeQuery(query2)); + FoodmartTestContextImpl.toString(testContext.executeQuery(query2)); assertEquals( measureValues(resultString1), measureValues(resultString2)); @@ -435,8 +435,8 @@ public static void verifySameNativeAndNot( Result resultNonNative = context.executeQuery(query); assertEquals( - TestContext.toString(resultNative), - TestContext.toString(resultNonNative), + FoodmartTestContextImpl.toString(resultNative), + FoodmartTestContextImpl.toString(resultNonNative), message); propSaver.reset(); diff --git a/mondrian/src/test/java/mondrian/test/FoodmartTestContextImpl.java b/mondrian/src/test/java/mondrian/test/FoodmartTestContextImpl.java new file mode 100644 index 0000000000..a794b035a4 --- /dev/null +++ b/mondrian/src/test/java/mondrian/test/FoodmartTestContextImpl.java @@ -0,0 +1,2262 @@ +/* +// This software is subject to the terms of the Eclipse Public License v1.0 +// Agreement, available at the following URL: +// http://www.eclipse.org/legal/epl-v10.html. +// You must accept the terms of that agreement to use this software. +// +// Copyright (C) 2002-2005 Julian Hyde +// Copyright (C) 2005-2020 Hitachi Vantara and others +// All Rights Reserved. +*/ +package mondrian.test; + + +import java.io.File; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.ref.SoftReference; +import java.lang.reflect.Proxy; +import java.net.MalformedURLException; +import java.net.URL; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.AbstractList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Formatter; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Pattern; + +import javax.sql.DataSource; + +import org.olap4j.CellSet; +import org.olap4j.CellSetAxis; +import org.olap4j.OlapConnection; +import org.olap4j.OlapStatement; +import org.olap4j.OlapWrapper; +import org.olap4j.driver.xmla.XmlaOlap4jDriver; +import org.olap4j.impl.CoordinateIterator; +import org.olap4j.layout.TraditionalCellSetFormatter; + +import junit.framework.Assert; +import junit.framework.ComparisonFailure; +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import mondrian.calc.Calc; +import mondrian.calc.CalcWriter; +import mondrian.calc.ResultStyle; +import mondrian.olap.Axis; +import mondrian.olap.CacheControl; +import mondrian.olap.Cell; +import mondrian.olap.Connection; +import mondrian.olap.DriverManager; +import mondrian.olap.Exp; +import mondrian.olap.Formula; +import mondrian.olap.Hierarchy; +import mondrian.olap.Member; +import mondrian.olap.MondrianProperties; +import mondrian.olap.Position; +import mondrian.olap.Query; +import mondrian.olap.Result; +import mondrian.olap.Util; +import mondrian.olap.fun.FunUtil; +import mondrian.olap4j.MondrianInprocProxy; +import mondrian.resource.MondrianResource; +import mondrian.rolap.RolapConnectionProperties; +import mondrian.rolap.RolapCube; +import mondrian.rolap.RolapHierarchy; +import mondrian.rolap.RolapUtil; +import mondrian.spi.Dialect; +import mondrian.spi.DialectManager; +import mondrian.spi.DynamicSchemaProcessor; +import mondrian.spi.impl.FilterDynamicSchemaProcessor; +import mondrian.util.DelegatingInvocationHandler; + +/** + * FoodmartTestContextImpl is a singleton class which contains the information + * necessary to run mondrian tests (otherwise we'd have to pass this information into the constructor of TestCases). + * + *

The singleton instance (retrieved via the {@link #instance()} method) + * contains a connection to the FoodMart database, and runs expressions in the context of the Sales cube. + * + *

Using the {@link DelegatingTestContext} subclass, you can create derived + * classes which use a different connection or a different cube. + * + * @author jhyde + * @since 29 March, 2002 + */ +public class FoodmartTestContextImpl implements TestContext { + private static FoodmartTestContextImpl instance; // the singleton + private PrintWriter pw; + + private SoftReference connectionRef; + + private Dialect dialect; + + protected static final String nl = Util.nl; + private static final String indent = " "; + private static final String lineBreak = "\"," + nl + "\""; + private static final String lineBreak2 = "\\\\n\"" + nl + indent + "+ \""; + private static final Pattern LineBreakPattern = + Pattern.compile( "\r\n|\r|\n" ); + private static final Pattern TabPattern = Pattern.compile( "\t" ); + private static final String[] AllHiers = { + "[Measures]", + "[Store]", + "[Store Size in SQFT]", + "[Store Type]", + "[Time]", + MondrianProperties.instance().SsasCompatibleNaming.get() ? "[Time].[Weekly]" : "[Time.Weekly]", + "[Product]", + "[Promotion Media]", + "[Promotions]", + "[Customers]", + "[Education Level]", + "[Gender]", + "[Marital Status]", + "[Yearly Income]" + }; + private static String unadulteratedFoodMartSchema; + + /** + * Retrieves the singleton (instantiating if necessary). + */ + public static synchronized FoodmartTestContextImpl instance() { + if ( instance == null ) { + instance = new FoodmartTestContextImpl(); + } + return instance; + } + + /** + * Creates a TestContext. + */ + protected FoodmartTestContextImpl() { + // Run all tests in the US locale, not the system default locale, + // because the results all assume the US locale. + MondrianResource.setThreadLocale( Locale.US ); + + + + this.pw = new PrintWriter( System.out, true ); + } + + /** + * Returns the connect string by which the unit tests can talk to the FoodMart database. + * + *

In the base class, the result is the same as the static method + * {@link #getDefaultConnectString}. If a derived class overrides {@link #getConnectionProperties()}, the result of + * this method will change also. + */ + @Override +public final String getConnectString() { + return getConnectionProperties().toString(); + } + + /** + * Constructs a connect string by which the unit tests can talk to the FoodMart database. + *

+ * The algorithm is as follows:

    + *
  • Starts with {@link MondrianProperties#TestConnectString}, if it is + * set.
  • + *
  • If {@link MondrianProperties#FoodmartJdbcURL} is set, this + * overrides the Jdbc property.
  • + *
  • If the catalog URL is unset or invalid, it assumes that + * we are at the root of the source tree, and references + * demo/FoodMart.xml
  • . + *
+ */ + public static String getDefaultConnectString() { + String connectString = + MondrianProperties.instance().TestConnectString.get(); + final Util.PropertyList connectProperties; + if ( connectString == null || connectString.equals( "" ) ) { + connectProperties = new Util.PropertyList(); + connectProperties.put( "Provider", "mondrian" ); + } else { + connectProperties = Util.parseConnectString( connectString ); + } + String jdbcURL = MondrianProperties.instance().FoodmartJdbcURL.get(); + if ( jdbcURL != null ) { + connectProperties.put( "Jdbc", jdbcURL ); + } + String jdbcUser = MondrianProperties.instance().TestJdbcUser.get(); + if ( jdbcUser != null ) { + connectProperties.put( "JdbcUser", jdbcUser ); + } + String jdbcPassword = + MondrianProperties.instance().TestJdbcPassword.get(); + if ( jdbcPassword != null ) { + connectProperties.put( "JdbcPassword", jdbcPassword ); + } + + // Find the catalog. Use the URL specified in the connect string, if + // it is specified and is valid. Otherwise, reference FoodMart.xml + // assuming we are at the root of the source tree. + URL catalogURL = null; + String catalog = connectProperties.get( "catalog" ); + if ( catalog != null ) { + try { + catalogURL = new URL( catalog ); + } catch ( MalformedURLException e ) { + // ignore + } + } + if ( catalogURL == null ) { + // Works if we are running in root directory of source tree + File file = new File( "demo/FoodMart.xml" ); + if ( !file.exists() ) { + // Works if we are running in bin directory of runtime env + file = new File( "../demo/FoodMart.xml" ); + } + try { + catalogURL = Util.toURL( file ); + } catch ( MalformedURLException e ) { + throw new Error( e.getMessage() ); + } + } + connectProperties.put( "catalog", catalogURL.toString() ); + return connectProperties.toString(); + } + + @Override +public synchronized void flushSchemaCache() { + // it's pointless to flush the schema cache if we + // have a handle on the connection object already + getConnection().getCacheControl( null ).flushSchemaCache(); + } + + /** + * Returns the connection to run queries. + * + *

When invoked on the default TestContext instance, returns a connection + * to the FoodMart database. + */ + @Override +public synchronized Connection getConnection() { + if ( connectionRef != null ) { + Connection connection = connectionRef.get(); + if ( connection != null ) { + return connection; + } + } + final Connection connection = + DriverManager.getConnection( + getConnectionProperties(), + null, + null ); + connectionRef = new SoftReference( connection ); + return connection; + } + + /** + * Returns a connection to the FoodMart database with a dynamic schema processor and disables use of RolapSchema + * Pool. + */ + public TestContext withSchemaProcessor( + Class dynProcClass ) { + final Util.PropertyList properties = getConnectionProperties().clone(); + properties.put( + RolapConnectionProperties.DynamicSchemaProcessor.name(), + dynProcClass.getName() ); + properties.put( + RolapConnectionProperties.UseSchemaPool.name(), + "false" ); + return withProperties( properties ); + } + + /** + * Returns a {@link FoodmartTestContextImpl} similar to this one, but which uses a fresh connection. + * + * @return Test context which uses the a fresh connection + * @see #withSchemaPool(boolean) + */ + public final TestContext withFreshConnection() { + final Connection connection = withSchemaPool( false ).getConnection(); + return withConnection( connection ); + } + + public TestContext withSchemaPool( boolean usePool ) { + final Util.PropertyList properties = getConnectionProperties().clone(); + properties.put( + RolapConnectionProperties.UseSchemaPool.name(), + Boolean.toString( usePool ) ); + return withProperties( properties ); + } + + @Override +public Util.PropertyList getConnectionProperties() { + final Util.PropertyList propertyList = + Util.parseConnectString( getDefaultConnectString() ); + if ( MondrianProperties.instance().TestHighCardinalityDimensionList + .get() != null + && propertyList.get( + RolapConnectionProperties.DynamicSchemaProcessor.name() ) + == null ) { + propertyList.put( + RolapConnectionProperties.DynamicSchemaProcessor.name(), + HighCardDynamicSchemaProcessor.class.getName() ); + } + return propertyList; + } + + /** + * Returns a the XML of the current schema with added parameters and cube definitions. + */ + public static String getSchema( + String parameterDefs, + String cubeDefs, + String virtualCubeDefs, + String namedSetDefs, + String udfDefs, + String roleDefs ) { + // First, get the unadulterated schema. + String s = getRawFoodMartSchema(); + + // Add parameter definitions, if specified. + if ( parameterDefs != null ) { + int i = s.indexOf( "" ); + s = s.substring( 0, i ) + + parameterDefs + + s.substring( i ); + } + + // Add cube definitions, if specified. + if ( cubeDefs != null ) { + int i = + s.indexOf( + "" ); + s = s.substring( 0, i ) + + cubeDefs + + s.substring( i ); + } + + // Add virtual cube definitions, if specified. + if ( virtualCubeDefs != null ) { + int i = s.indexOf( + "" ); + s = s.substring( 0, i ) + + virtualCubeDefs + + s.substring( i ); + } + + // Add named set definitions, if specified. Schema-level named sets + // occur after and and before elements. + if ( namedSetDefs != null ) { + int i = s.indexOf( "" ); + } + s = s.substring( 0, i ) + + namedSetDefs + + s.substring( i ); + } + + // Add definitions of roles, if specified. + if ( roleDefs != null ) { + int i = s.indexOf( "" ); + } + s = s.substring( 0, i ) + + roleDefs + + s.substring( i ); + } + + // Add definitions of user-defined functions, if specified. + if ( udfDefs != null ) { + int i = s.indexOf( "" ); + s = s.substring( 0, i ) + + udfDefs + + s.substring( i ); + } + return s; + } + + /** + * Returns the definition of the "FoodMart" schema as stored in {@code FoodMart.xml}. + * + * @return XML definition of the FoodMart schema + */ + public static String getRawFoodMartSchema() { + synchronized ( SnoopingSchemaProcessor.class ) { + if ( unadulteratedFoodMartSchema == null ) { + unadulteratedFoodMartSchema = instance().getRawSchema(); + } + } + return unadulteratedFoodMartSchema; + } + + /** + * Returns the definition of the schema. + * + * @return XML definition of the FoodMart schema + */ + @Override +public String getRawSchema() { + final Connection connection = + withSchemaProcessor( SnoopingSchemaProcessor.class ) + .getConnection(); + connection.close(); + String schema = SnoopingSchemaProcessor.THREAD_RESULT.get(); + Util.threadLocalRemove( SnoopingSchemaProcessor.THREAD_RESULT ); + return schema; + } + + /** + * Returns a the XML of the foodmart schema, adding dimension definitions to the definition of a given cube. + */ + private static String substituteSchema( + String rawSchema, + String cubeName, + String dimensionDefs, + String measureDefs, + String memberDefs, + String namedSetDefs, + String defaultMeasure ) { + String s = rawSchema; + + // Search for the or element. + int h = s.indexOf( "", h ); + } + + // Add dimension definitions, if specified. + if ( dimensionDefs != null ) { + int i = s.indexOf( " end ) { + i = end; + } + s = s.substring( 0, i ) + + measureDefs + + s.substring( i ); + + // Same for VirtualCubeMeasure + if ( i == end ) { + i = s.indexOf( " end ) { + i = end; + } + s = s.substring( 0, i ) + + measureDefs + + s.substring( i ); + } + } + + // Add calculated member definitions, if specified. + if ( memberDefs != null ) { + int i = s.indexOf( " end ) { + i = end; + } + s = s.substring( 0, i ) + + memberDefs + + s.substring( i ); + } + + if ( namedSetDefs != null ) { + int i = s.indexOf( " end ) { + i = end; + } + s = s.substring( 0, i ) + + namedSetDefs + + s.substring( i ); + } + if ( defaultMeasure != null ) { + s = s.replaceFirst( + "(" + cubeName + ".*)defaultMeasure=\"[^\"]*\"", + "$1defaultMeasure=\"" + defaultMeasure + "\"" ); + } + + return s; + } + + /** + * Executes a query. + * + * @param queryString Query string + */ + @Override +public Result executeQuery( String queryString ) { + Connection connection = getConnection(); + queryString = upgradeQuery( queryString ); + Query query = connection.parseQuery( queryString ); + final Result result = connection.execute( query ); + + // If we're deep testing, check that we never return the dummy null + // value when cells are null. TestExpDependencies isn't the perfect + // switch to enable this, but it will do for now. + if ( MondrianProperties.instance().TestExpDependencies.booleanValue() ) { + assertResultValid( result ); + } + return result; + } + + @Override +public ResultSet executeStatement( String queryString ) throws SQLException { + OlapConnection connection = getOlap4jConnection(); + queryString = upgradeQuery( queryString ); + OlapStatement stmt = connection.createStatement(); + return stmt.executeQuery( queryString ); + } + + /** + * Executes a query using olap4j. + */ + @Override +public CellSet executeOlap4jQuery( String queryString ) throws SQLException { + OlapConnection connection = getOlap4jConnection(); + queryString = upgradeQuery( queryString ); + OlapStatement stmt = connection.createStatement(); + final CellSet cellSet = stmt.executeOlapQuery( queryString ); + + // If we're deep testing, check that we never return the dummy null + // value when cells are null. TestExpDependencies isn't the perfect + // switch to enable this, but it will do for now. + if ( MondrianProperties.instance().TestExpDependencies.booleanValue() ) { + assertCellSetValid( cellSet ); + } + return cellSet; + } + + @Override +public CellSet executeOlap4jXmlaQuery( String queryString ) + throws SQLException { + String schema = getConnectionProperties() + .get( RolapConnectionProperties.CatalogContent.name() ); + if ( schema == null ) { + schema = getRawSchema(); + } + // TODO: Need to better handle semicolons in schema content. + // Util.parseValue does not appear to allow escaping them. + schema = schema.replace( """, "" ).replace( ";", "" ); + + String Jdbc = getConnectionProperties() + .get( RolapConnectionProperties.Jdbc.name() ); + + String cookie = XmlaOlap4jDriver.nextCookie(); + Map catalogs = new HashMap(); + catalogs.put( "FoodMart", "" ); + XmlaOlap4jDriver.PROXY_MAP.put( + cookie, new MondrianInprocProxy( + catalogs, + "jdbc:mondrian:Server=http://whatever;" + + "Jdbc=" + Jdbc + ";TestProxyCookie=" + + cookie + + ";CatalogContent=" + schema ) ); + try { + Class.forName( "org.olap4j.driver.xmla.XmlaOlap4jDriver" ); + } catch ( ClassNotFoundException e ) { + throw new RuntimeException( "oops", e ); + } + Properties info = new Properties(); + info.setProperty( + XmlaOlap4jDriver.Property.CATALOG.name(), "FoodMart" ); + java.sql.Connection connection = java.sql.DriverManager.getConnection( + "jdbc:xmla:Server=http://whatever;Catalog=FoodMart;TestProxyCookie=" + + cookie, + info ); + OlapConnection olapConnection = + connection.unwrap( OlapConnection.class ); + OlapStatement statement = olapConnection.createStatement(); + return statement.executeOlapQuery( queryString ); + } + + + /** + * Checks that a {@link Result} is valid. + * + * @param result Query result + */ + private static void assertResultValid( Result result ) { + for ( Cell cell : cellIter( result ) ) { + final Object value = cell.getValue(); + + // Check that the dummy value used to represent null cells never + // leaks into the outside world. + Assert.assertNotSame( value, Util.nullValue ); + Assert.assertFalse( + value instanceof Number + && ( (Number) value ).doubleValue() == FunUtil.DoubleNull ); + + // Similarly empty values. + Assert.assertNotSame( value, Util.EmptyValue ); + Assert.assertFalse( + value instanceof Number + && ( (Number) value ).doubleValue() == FunUtil.DoubleEmpty ); + + // Cells should be null if and only if they are null or empty. + if ( cell.getValue() == null ) { + Assert.assertTrue( cell.isNull() ); + } else { + Assert.assertFalse( cell.isNull() ); + } + } + + // There should be no null members. + for ( Axis axis : result.getAxes() ) { + for ( Position position : axis.getPositions() ) { + for ( Member member : position ) { + Assert.assertNotNull( member ); + } + } + } + } + + /** + * Checks that a {@link CellSet} is valid. + * + * @param cellSet Cell set + */ + private static void assertCellSetValid( CellSet cellSet ) { + for ( org.olap4j.Cell cell : cellIter( cellSet ) ) { + final Object value = cell.getValue(); + + // Check that the dummy value used to represent null cells never + // leaks into the outside world. + Assert.assertNotSame( value, Util.nullValue ); + Assert.assertFalse( + value instanceof Number + && ( (Number) value ).doubleValue() == FunUtil.DoubleNull ); + + // Similarly empty values. + Assert.assertNotSame( value, Util.EmptyValue ); + Assert.assertFalse( + value instanceof Number + && ( (Number) value ).doubleValue() == FunUtil.DoubleEmpty ); + + // Cells should be null if and only if they are null or empty. + if ( cell.getValue() == null ) { + Assert.assertTrue( cell.isNull() ); + } else { + Assert.assertFalse( cell.isNull() ); + } + } + + // There should be no null members. + for ( CellSetAxis axis : cellSet.getAxes() ) { + for ( org.olap4j.Position position : axis.getPositions() ) { + for ( org.olap4j.metadata.Member member : position.getMembers() ) { + Assert.assertNotNull( member ); + } + } + } + } + + /** + * Returns an iterator over cells in a result. + */ + static Iterable cellIter( final Result result ) { + return new Iterable() { + public Iterator iterator() { + int[] axisDimensions = new int[ result.getAxes().length ]; + int k = 0; + for ( Axis axis : result.getAxes() ) { + axisDimensions[ k++ ] = axis.getPositions().size(); + } + final CoordinateIterator + coordIter = new CoordinateIterator( axisDimensions ); + return new Iterator() { + public boolean hasNext() { + return coordIter.hasNext(); + } + + public Cell next() { + final int[] ints = coordIter.next(); + return result.getCell( ints ); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + /** + * Returns an iterator over cells in an olap4j cell set. + */ + static Iterable cellIter( final CellSet cellSet ) { + return new Iterable() { + public Iterator iterator() { + int[] axisDimensions = new int[ cellSet.getAxes().size() ]; + int k = 0; + for ( CellSetAxis axis : cellSet.getAxes() ) { + axisDimensions[ k++ ] = axis.getPositions().size(); + } + final CoordinateIterator + coordIter = new CoordinateIterator( axisDimensions ); + return new Iterator() { + public boolean hasNext() { + return coordIter.hasNext(); + } + + public org.olap4j.Cell next() { + final int[] ints = coordIter.next(); + final List list = + new AbstractList() { + public Integer get( int index ) { + return ints[ index ]; + } + + public int size() { + return ints.length; + } + }; + return cellSet.getCell( + list ); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + /** + * Executes a query, and asserts that it throws an exception which contains the given pattern. + * + * @param queryString Query string + * @param pattern Pattern which exception must match + */ + @Override +public void assertQueryThrows( String queryString, String pattern ) { + Throwable throwable; + try { + Result result = executeQuery( queryString ); + Util.discard( result ); + throwable = null; + } catch ( Throwable e ) { + throwable = e; + } + checkThrowable( throwable, pattern ); + } + + /** + * Executes an expression, and asserts that it gives an error which contains a particular pattern. The error might + * occur during parsing, or might be contained within the cell value. + */ + @Override +public void assertExprThrows( String expression, String pattern ) { + Throwable throwable = null; + try { + String cubeName = getDefaultCubeName(); + if ( cubeName.indexOf( ' ' ) >= 0 ) { + cubeName = Util.quoteMdxIdentifier( cubeName ); + } + expression = Util.replace( expression, "'", "''" ); + Result result = executeQuery( + "with member [Measures].[Foo] as '" + + expression + + "' select {[Measures].[Foo]} on columns from " + + cubeName ); + Cell cell = result.getCell( new int[] { 0 } ); + if ( cell.isError() ) { + throwable = (Throwable) cell.getValue(); + } + } catch ( Throwable e ) { + throwable = e; + } + checkThrowable( throwable, pattern ); + } + + /** + * Returns the name of the default cube. + * + *

Tests which evaluate scalar expressions, such as + * {@link #assertExprReturns(String, String)}, generate queries against this cube. + * + * @return the name of the default cube + */ + @Override +public String getDefaultCubeName() { + return "Sales"; + } + + /** + * Executes the expression in the context of the cube indicated by + * cubeName, and returns the result as a Cell. + * + * @param expression The expression to evaluate + * @return Cell which is the result of the expression + */ + @Override +public Cell executeExprRaw( String expression ) { + final String queryString = generateExpression( expression ); + Result result = executeQuery( queryString ); + return result.getCell( new int[] { 0 } ); + } + + private String generateExpression( String expression ) { + String cubeName = getDefaultCubeName(); + if ( cubeName.indexOf( ' ' ) >= 0 ) { + cubeName = Util.quoteMdxIdentifier( cubeName ); + } + return + "with member [Measures].[Foo] as " + + Util.singleQuoteString( expression ) + + " select {[Measures].[Foo]} on columns from " + cubeName; + } + + /** + * Executes an expression and asserts that it returns a given result. + */ + @Override +public void assertExprReturns( String expression, String expected ) { + final Cell cell = executeExprRaw( expression ); + if ( expected == null ) { + expected = ""; // null values are formatted as empty string + } + assertEqualsVerbose( expected, cell.getFormattedValue() ); + } + + /** + * Asserts that an expression, with a given set of parameter bindings, returns a given result. + * + * @param expr Scalar MDX expression + * @param expected Expected result + * @param paramValues Array of parameter names and values + */ + @Override +public void assertParameterizedExprReturns( + String expr, + String expected, + Object... paramValues ) { + Connection connection = getConnection(); + String queryString = generateExpression( expr ); + Query query = connection.parseQuery( queryString ); + assert paramValues.length % 2 == 0; + for ( int i = 0; i < paramValues.length; ) { + final String paramName = (String) paramValues[ i++ ]; + final Object value = paramValues[ i++ ]; + query.setParameter( paramName, value ); + } + final Result result = connection.execute( query ); + final Cell cell = result.getCell( new int[] { 0 } ); + + if ( expected == null ) { + expected = ""; // null values are formatted as empty string + } + assertEqualsVerbose( expected, cell.getFormattedValue() ); + } + + /** + * Executes a query with a given expression on an axis, and asserts that it returns the expected string. + */ + @Override +public void assertAxisReturns( + String expression, + String expected ) { + Axis axis = executeAxis( expression ); + assertEqualsVerbose( + expected, + upgradeActual( toString( axis.getPositions() ) ) ); + } + + /** + * Massages the actual result of executing a query to handle differences in unique names betweeen old and new + * behavior. + * + *

Even though the new naming is not enabled by default, reference logs + * should be in terms of the new naming. + * + * @param actual Actual result + * @return Expected result massaged for backwards compatibility + * @see mondrian.olap.MondrianProperties#SsasCompatibleNaming + */ + @Override +public String upgradeActual( String actual ) { + if ( !MondrianProperties.instance().SsasCompatibleNaming.get() ) { + actual = Util.replace( + actual, + "[Time.Weekly]", + "[Time].[Weekly]" ); + actual = Util.replace( + actual, + "[All Time.Weeklys]", + "[All Weeklys]" ); + actual = Util.replace( + actual, + "Time.Weekly", + "Weekly" ); + + // for a few tests in SchemaTest + actual = Util.replace( + actual, + "[Store.MyHierarchy]", + "[Store].[MyHierarchy]" ); + actual = Util.replace( + actual, + "[All Store.MyHierarchys]", + "[All MyHierarchys]" ); + actual = Util.replace( + actual, + "[Store2].[All Store2s]", + "[Store2].[Store].[All Stores]" ); + actual = Util.replace( + actual, + "[Store Type 2.Store Type 2].[All Store Type 2.Store Type 2s]", + "[Store Type 2].[All Store Type 2s]" ); + actual = Util.replace( + actual, + "[TIME.CALENDAR]", + "[TIME].[CALENDAR]" ); + actual = Util.replace( + actual, + "true", + "1" ); + actual = Util.replace( + actual, + "80000.0000", + "80000" ); + } + return actual; + } + + /** + * Massages an MDX query to handle differences in unique names betweeen old and new behavior. + * + *

The main difference addressed is with level naming. The problem + * arises when dimension, hierarchy and level have the same name:

    + * + *
  • In old behavior, the [Gender].[Gender] represents the Gender level, + * and [Gender].[Gender].[Gender] is invalid. + * + *
  • In new behavior, [Gender].[Gender] represents the Gender hierarchy, + * and [Gender].[Gender].[Gender].members represents the Gender level. + *

+ * + *

So, {@code upgradeQuery("[Gender]")} returns + * "[Gender].[Gender]" for old behavior, "[Gender].[Gender].[Gender]" for new behavior.

+ * + * @param queryString Original query + * @return Massaged query for backwards compatibility + * @see mondrian.olap.MondrianProperties#SsasCompatibleNaming + */ + @Override +public String upgradeQuery( String queryString ) { + if ( MondrianProperties.instance().SsasCompatibleNaming.get() ) { + String[] names = { + "[Gender]", + "[Education Level]", + "[Marital Status]", + "[Store Type]", + "[Yearly Income]", + }; + for ( String name : names ) { + queryString = Util.replace( + queryString, + name + "." + name, + name + "." + name + "." + name ); + } + queryString = Util.replace( + queryString, + "[Time.Weekly].[All Time.Weeklys]", + "[Time].[Weekly].[All Weeklys]" ); + } + return queryString; + } + + /** + * Compiles a scalar expression in the context of the default cube. + * + * @param expression The expression to evaluate + * @param scalar Whether the expression is scalar + * @return String form of the program + */ + @Override +public String compileExpression( String expression, final boolean scalar ) { + String cubeName = getDefaultCubeName(); + if ( cubeName.indexOf( ' ' ) >= 0 ) { + cubeName = Util.quoteMdxIdentifier( cubeName ); + } + final String queryString; + if ( scalar ) { + queryString = + "with member [Measures].[Foo] as " + + Util.singleQuoteString( expression ) + + " select {[Measures].[Foo]} on columns from " + cubeName; + } else { + queryString = + "SELECT {" + expression + "} ON COLUMNS FROM " + cubeName; + } + Connection connection = getConnection(); + Query query = connection.parseQuery( queryString ); + final Exp exp; + if ( scalar ) { + exp = query.getFormulas()[ 0 ].getExpression(); + } else { + exp = query.getAxes()[ 0 ].getSet(); + } + final Calc calc = query.compileExpression( exp, scalar, null ); + final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter( sw ); + final CalcWriter calcWriter = new CalcWriter( pw, false ); + calc.accept( calcWriter ); + pw.flush(); + return sw.toString(); + } + + /** + * Executes a set expression which is expected to return 0 or 1 members. It is an error if the expression returns + * tuples (as opposed to members), or if it returns two or more members. + * + * @param expression Expression string + * @return Null if axis returns the empty set, member if axis returns one member. Throws otherwise. + */ + @Override +public Member executeSingletonAxis( String expression ) { + final String cubeName = getDefaultCubeName(); + Result result = executeQuery( + "select {" + expression + "} on columns from " + cubeName ); + Axis axis = result.getAxes()[ 0 ]; + switch ( axis.getPositions().size() ) { + case 0: + // The mdx "{...}" operator eliminates null members (that is, + // members for which member.isNull() is true). So if "expression" + // yielded just the null member, the array will be empty. + return null; + case 1: + // Java nulls should never happen during expression evaluation. + Position position = axis.getPositions().get( 0 ); + Util.assertTrue( position.size() == 1 ); + Member member = position.get( 0 ); + Util.assertTrue( member != null ); + return member; + default: + throw Util.newInternal( + "expression " + expression + + " yielded " + axis.getPositions().size() + " positions" ); + } + } + + /** + * Executes a query with a given expression on an axis, and returns the whole axis. + */ + @Override +public Axis executeAxis( String expression ) { + Result result = executeQuery( + "select {" + expression + + "} on columns from " + getDefaultCubeName() ); + return result.getAxes()[ 0 ]; + } + + /** + * Executes a query with a given expression on an axis, and asserts that it throws an error which matches a particular + * pattern. The expression is evaulated against the default cube. + */ + @Override +public void assertAxisThrows( + String expression, + String pattern ) { + Throwable throwable = null; + Connection connection = getConnection(); + try { + final String cubeName = getDefaultCubeName(); + final String queryString = + "select {" + expression + "} on columns from " + cubeName; + Query query = connection.parseQuery( queryString ); + connection.execute( query ); + } catch ( Throwable e ) { + throwable = e; + } + checkThrowable( throwable, pattern ); + } + + public static void checkThrowable( Throwable throwable, String pattern ) { + if ( throwable == null ) { + Assert.fail( "query did not yield an exception" ); + } + String stackTrace = getStackTrace( throwable ); + if ( stackTrace.indexOf( pattern ) < 0 ) { + Assert.fail( + "query's error does not match pattern '" + pattern + + "'; error is [" + stackTrace + "]" ); + } + } + + /** + * Returns the output writer. + */ + @Override +public PrintWriter getWriter() { + return pw; + } + + /** + * Executes a query and checks that the result is a given string. + */ + @Override +public void assertQueryReturns( String query, String desiredResult ) { + Result result = executeQuery( query ); + String resultString = toString( result ); + if ( desiredResult != null ) { + assertEqualsVerbose( + desiredResult, + upgradeActual( resultString ) ); + } + } + + /** + * Executes a query and checks that the result is a given string, displaying a message if result does not match + * desiredResult. + */ + @Override +public void assertQueryReturns( + String message, String query, String desiredResult ) { + Result result = executeQuery( query ); + String resultString = toString( result ); + if ( desiredResult != null ) { + assertEqualsVerbose( + desiredResult, + upgradeActual( resultString ), + true, message ); + } + } + + + /** + * Executes a very simple query. + * + *

This forces the schema to be loaded and performs a basic sanity check. + * If this is a negative schema test, causes schema validation errors to be thrown. + */ + @Override +public void assertSimpleQuery() { + assertQueryReturns( + "select from [Sales]", + "Axis #0:\n" + + "{}\n" + + "266,773" ); + } + + /** + * Checks that an actual string matches an expected string. + * + *

If they do not, throws a {@link junit.framework.ComparisonFailure} and + * prints the difference, including the actual string as an easily pasted Java string literal. + */ + public static void assertEqualsVerbose( + String expected, + String actual ) { + assertEqualsVerbose( expected, actual, true, null ); + } + + /** + * Checks that an actual string matches an expected string. + * + *

If they do not, throws a {@link ComparisonFailure} and prints the + * difference, including the actual string as an easily pasted Java string literal. + * + * @param expected Expected string + * @param actual Actual string + * @param java Whether to generate actual string as a Java string literal if the values are not equal + * @param message Message to display, optional + */ + public static void assertEqualsVerbose( + String expected, + String actual, + boolean java, + String message ) { + assertEqualsVerbose( + fold( expected ), actual, java, message ); + } + + /** + * Checks that an actual string matches an expected string. + * + *

If they do not, throws a {@link ComparisonFailure} and prints the + * difference, including the actual string as an easily pasted Java string literal. + * + * @param safeExpected Expected string, where all line endings have been converted into platform-specific line + * endings + * @param actual Actual string + * @param java Whether to generate actual string as a Java string literal if the values are not equal + * @param message Message to display, optional + */ + public static void assertEqualsVerbose( + SafeString safeExpected, + String actual, + boolean java, + String message ) { + String expected = safeExpected == null ? null : safeExpected.s; + if ( ( expected == null ) && ( actual == null ) ) { + return; + } + if ( ( expected != null ) && expected.equals( actual ) ) { + return; + } + if ( message == null ) { + message = ""; + } else { + message += nl; + } + message += + "Expected:" + nl + expected + nl + + "Actual:" + nl + actual + nl; + if ( java ) { + message += "Actual java:" + nl + toJavaString( actual ) + nl; + } + throw new ComparisonFailure( message, expected, actual ); + } + + /** + * Checks that an actual string matches an expected string. Ignores the difference of anonymous class names in + * "mondrian...." package. + * + *

If they do not, throws a {@link junit.framework.ComparisonFailure} and + * prints the difference, including the actual string as an easily pasted Java string literal. + */ + public static void assertStubbedEqualsVerbose( + String expected, + String actual ) { + assertEqualsVerbose( + stubAnonymousClasses( expected ), + stubAnonymousClasses( actual ) ); + } + + private static String toJavaString( String s ) { + // Convert [string with "quotes" split + // across lines] + // into ["string with \"quotes\" split\n" + // + "across lines + // + s = Util.replace( s, "\"", "\\\"" ); + s = LineBreakPattern.matcher( s ).replaceAll( lineBreak2 ); + s = TabPattern.matcher( s ).replaceAll( "\\\\t" ); + s = "\"" + s + "\""; + String spurious = nl + indent + "+ \"\""; + if ( s.endsWith( spurious ) ) { + s = s.substring( 0, s.length() - spurious.length() ); + } + return s; + } + + /** + * Checks that an actual string matches an expected pattern. If they do not, throws a {@link ComparisonFailure} and + * prints the difference, including the actual string as an easily pasted Java string literal. + */ + @Override +public void assertMatchesVerbose( + Pattern expected, + String actual ) { + Util.assertPrecondition( expected != null, "expected != null" ); + if ( expected.matcher( actual ).matches() ) { + return; + } + String s = actual; + + // Convert [string with "quotes" split + // across lines] + // into ["string with \"quotes\" split" + nl + + // "across lines + // + s = Util.replace( s, "\"", "\\\"" ); + s = LineBreakPattern.matcher( s ).replaceAll( lineBreak ); + s = TabPattern.matcher( s ).replaceAll( "\\\\t" ); + s = "\"" + s + "\""; + final String spurious = " + " + nl + "\"\""; + if ( s.endsWith( spurious ) ) { + s = s.substring( 0, s.length() - spurious.length() ); + } + String message = + "Expected pattern:" + nl + expected + nl + + "Actual: " + nl + actual + nl + + "Actual java: " + nl + s + nl; + throw new ComparisonFailure( message, expected.pattern(), actual ); + } + + /** + * Converts a {@link Throwable} to a stack trace. + */ + public static String getStackTrace( Throwable e ) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter( sw ); + e.printStackTrace( pw ); + pw.flush(); + return sw.toString(); + } + + /** + * Converts a {@link mondrian.olap.Result} to text in traditional format. + * + *

For more exotic formats, see + * {@link org.olap4j.layout.CellSetFormatter}. + * + * @param result Query result + * @return Result as text + */ + public static String toString( Result result ) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter( sw ); + result.print( pw ); + pw.flush(); + return sw.toString(); + } + + /** + * Converts a {@link CellSet} to text in traditional format. + * + *

For more exotic formats, see + * {@link org.olap4j.layout.CellSetFormatter}. + * + * @param cellSet Query result + * @return Result as text + */ + public static String toString( CellSet cellSet ) { + final StringWriter sw = new StringWriter(); + new TraditionalCellSetFormatter().format( + cellSet, + new PrintWriter( sw ) ); + return sw.toString(); + } + + /** + * Returns a test context whose {@link #getOlap4jConnection()} method always returns the same connection object, and + * which has an active {@link org.olap4j.Scenario}, thus enabling writeback. + * + * @return Test context with active scenario + */ + public final TestContext withScenario() { + return new DelegatingTestContext( this ) { + OlapConnection connection; + + public OlapConnection getOlap4jConnection() throws SQLException { + if ( connection == null ) { + connection = super.getOlap4jConnection(); + connection.setScenario( + connection.createScenario() ); + } + return connection; + } + }; + } + + /** + * Converts a set of positions into a string. Useful if you want to check that an axis has the results you expected. + */ + public static String toString( List positions ) { + StringBuilder buf = new StringBuilder(); + int i = 0; + for ( Position position : positions ) { + if ( i > 0 ) { + buf.append( nl ); + } + if ( position.size() != 1 ) { + buf.append( "{" ); + } + for ( int j = 0; j < position.size(); j++ ) { + Member member = position.get( j ); + if ( j > 0 ) { + buf.append( ", " ); + } + buf.append( member.getUniqueName() ); + } + if ( position.size() != 1 ) { + buf.append( "}" ); + } + i++; + } + return buf.toString(); + } + + /** + * Makes a copy of a suite, filtering certain tests. + * + * @param suite Test suite + * @param testPattern Regular expression of name of tests to include + * @return copy of test suite + */ + public static TestSuite copySuite( + TestSuite suite, + Util.Functor1 testPattern ) { + TestSuite newSuite = new TestSuite( suite.getName() ); + //noinspection unchecked + for ( Test test : Collections.list( (Enumeration) suite.tests() ) ) { + if ( !testPattern.apply( test ) ) { + continue; + } + if ( test instanceof TestCase ) { + newSuite.addTest( test ); + } else if ( test instanceof TestSuite ) { + TestSuite subSuite = copySuite( (TestSuite) test, testPattern ); + if ( subSuite.countTestCases() > 0 ) { + newSuite.addTest( subSuite ); + } + } else { + // some other kind of test + newSuite.addTest( test ); + } + } + return newSuite; + } + + @Override +public void close() { + // nothing + } + + /** + * Returns a {@link CacheControl}. + */ + @Override +public CacheControl getCacheControl() { + return getConnection().getCacheControl( null ); + } + + /** + * Wrapper around a string that indicates that all line endings have been converted to platform-specific line + * endings. + * + * @see FoodmartTestContextImpl#fold + */ + public static class SafeString { + public final String s; + + private SafeString( String s ) { + this.s = s; + } + } + + /** + * Replaces line-endings in a string with the platform-dependent equivalent. If the input string already has + * platform-dependent line endings, no replacements are made. + * + * @param string String whose line endings are to be made platform- dependent. Typically these are constant "expected + * value" string expressions where the linefeed is represented as linefeed "\n", but sometimes this + * method will receive strings created dynamically where the line endings are already appropriate for + * the platform. + * @return String where all linefeeds have been converted to platform-specific (CR+LF on Windows, LF on Unix/Linux) + */ + public static SafeString fold( String string ) { + if ( string == null ) { + return null; + } + if ( nl.equals( "\n" ) || string.indexOf( nl ) != -1 ) { + return new SafeString( string ); + } + return new SafeString( Util.replace( string, "\n", nl ) ); + } + + /** + * Reverses the effect of {@link #fold}; converts platform-specific line endings in a string info linefeeds. + * + * @param string String where all linefeeds have been converted to platform-specific (CR+LF on Windows, LF on + * Unix/Linux) + * @return String where line endings are represented as linefeed "\n" + */ + public static String unfold( String string ) { + if ( !nl.equals( "\n" ) ) { + string = Util.replace( string, nl, "\n" ); + } + if ( string == null ) { + return null; + } else { + return string; + } + } + + @Override +public synchronized Dialect getDialect() { + if ( dialect == null ) { + dialect = getDialectInternal(); + } + return dialect; + } + + private Dialect getDialectInternal() { + DataSource dataSource = getConnection().getDataSource(); + return DialectManager.createDialect( dataSource, null ); + } + + /** + * Creates a dialect without using a connection. + * + * @param product Database product + * @return dialect of an required persuasion + */ + public static Dialect getFakeDialect( Dialect.DatabaseProduct product ) { + final DatabaseMetaData metaData = + (DatabaseMetaData) Proxy.newProxyInstance( + null, + new Class[] { DatabaseMetaData.class }, + new DatabaseMetaDataInvocationHandler( product ) ); + final java.sql.Connection connection = + (java.sql.Connection) Proxy.newProxyInstance( + null, + new Class[] { java.sql.Connection.class }, + new ConnectionInvocationHandler( metaData ) ); + final Dialect dialect = DialectManager.createDialect( null, connection ); + assert dialect.getDatabaseProduct() == product; + return dialect; + } + + /** + * Checks that expected SQL equals actual SQL. Performs some normalization on the actual SQL to compensate for + * differences between dialects. + */ + @Override +public void assertSqlEquals( + String expectedSql, + String actualSql, + int expectedRows ) { + // if the actual SQL isn't in the current dialect we have some + // problems... probably with the dialectize method + assertEqualsVerbose( actualSql, dialectize( actualSql ) ); + + String transformedExpectedSql = removeQuotes( dialectize( expectedSql ) ) + .replaceAll( "\r\n", "\n" ); + String transformedActualSql = removeQuotes( actualSql ) + .replaceAll( "\r\n", "\n" ); + Assert.assertEquals( transformedExpectedSql, transformedActualSql ); + + checkSqlAgainstDatasource( actualSql, expectedRows ); + } + + private static String removeQuotes( String actualSql ) { + String transformedActualSql = actualSql.replaceAll( "`", "" ); + transformedActualSql = transformedActualSql.replaceAll( "\"", "" ); + return transformedActualSql; + } + + /** + * Converts a SQL string into the current dialect. + * + *

This is not intended to be a general purpose method: it looks for + * specific patterns known to occur in tests, in particular "=as=" and "fname + ' ' + lname". + * + * @param sql SQL string in generic dialect + * @return SQL string converted into current dialect + */ + private String dialectize( String sql ) { + final String search = "fname \\+ ' ' \\+ lname"; + final Dialect dialect = getDialect(); + final Dialect.DatabaseProduct databaseProduct = + dialect.getDatabaseProduct(); + switch ( databaseProduct ) { + case MYSQL: + case MARIADB: + // Mysql would generate "CONCAT(...)" + sql = sql.replaceAll( + search, + "CONCAT(`customer`.`fname`, ' ', `customer`.`lname`)" ); + break; + case POSTGRESQL: + case ORACLE: + case LUCIDDB: + case TERADATA: + sql = sql.replaceAll( + search, + "`fname` || ' ' || `lname`" ); + break; + case DERBY: + sql = sql.replaceAll( + search, + "`customer`.`fullname`" ); + break; + case INGRES: + sql = sql.replaceAll( + search, + "fullname" ); + break; + case DB2: + case DB2_AS400: + case DB2_OLD_AS400: + sql = sql.replaceAll( + search, + "CONCAT(CONCAT(`customer`.`fname`, ' '), `customer`.`lname`)" ); + break; + } + + if ( dialect.getDatabaseProduct() == Dialect.DatabaseProduct.ORACLE ) { + // " + tableQualifier + " + sql = sql.replaceAll( " =as= ", " " ); + } else { + sql = sql.replaceAll( " =as= ", " as " ); + } + return sql; + } + + private void checkSqlAgainstDatasource( + String actualSql, + int expectedRows ) { + Util.PropertyList connectProperties = getConnectionProperties(); + + java.sql.Connection jdbcConn = null; + Statement stmt = null; + ResultSet rs = null; + + try { + String jdbcDrivers = + connectProperties.get( + RolapConnectionProperties.JdbcDrivers.name() ); + if ( jdbcDrivers != null ) { + RolapUtil.loadDrivers( jdbcDrivers ); + } + final String jdbcDriversProp = + MondrianProperties.instance().JdbcDrivers.get(); + RolapUtil.loadDrivers( jdbcDriversProp ); + + jdbcConn = java.sql.DriverManager.getConnection( + connectProperties.get( RolapConnectionProperties.Jdbc.name() ), + connectProperties.get( + RolapConnectionProperties.JdbcUser.name() ), + connectProperties.get( + RolapConnectionProperties.JdbcPassword.name() ) ); + stmt = jdbcConn.createStatement(); + + if ( RolapUtil.SQL_LOGGER.isDebugEnabled() ) { + StringBuffer sqllog = new StringBuffer(); + sqllog.append( "mondrian.test.TestContext: executing sql [" ); + if ( actualSql.indexOf( '\n' ) >= 0 ) { + // SQL appears to be formatted as multiple lines. Make it + // start on its own line. + sqllog.append( "\n" ); + } + sqllog.append( actualSql ); + sqllog.append( ']' ); + RolapUtil.SQL_LOGGER.debug( sqllog.toString() ); + } + + long startTime = System.currentTimeMillis(); + rs = stmt.executeQuery( actualSql ); + long time = System.currentTimeMillis(); + final long execMs = time - startTime; + Util.addDatabaseTime( execMs ); + + RolapUtil.SQL_LOGGER.debug( ", exec " + execMs + " ms" ); + + int rows = 0; + while ( rs.next() ) { + rows++; + } + + Assert.assertEquals( "row count", expectedRows, rows ); + } catch ( SQLException e ) { + throw new RuntimeException( + "ERROR in SQL - invalid for database: " + + connectProperties.get( RolapConnectionProperties.Jdbc.name() ) + + "\n" + actualSql, + e ); + } finally { + try { + if ( rs != null ) { + rs.close(); + } + } catch ( Exception e1 ) { + // ignore + } + try { + if ( stmt != null ) { + stmt.close(); + } + } catch ( Exception e1 ) { + // ignore + } + try { + if ( jdbcConn != null ) { + jdbcConn.close(); + } + } catch ( Exception e1 ) { + // ignore + } + } + } + + /** + * Asserts that an MDX set-valued expression depends upon a given list of dimensions. + */ + @Override +public void assertSetExprDependsOn( String expr, String dimList ) { + // Construct a query, and mine it for a parsed expression. + // Use a fresh connection, because some tests define their own dims. + final Connection connection = getConnection(); + final String queryString = + "SELECT {" + expr + "} ON COLUMNS FROM [Sales]"; + final Query query = connection.parseQuery( queryString ); + query.resolve(); + final Exp expression = query.getAxes()[ 0 ].getSet(); + + // Build a list of the dimensions which the expression depends upon, + // and check that it is as expected. + checkDependsOn( query, expression, dimList, false ); + } + + /** + * Asserts that an MDX member-valued depends upon a given list of dimensions. + */ + @Override +public void assertMemberExprDependsOn( String expr, String dimList ) { + assertSetExprDependsOn( "{" + expr + "}", dimList ); + } + + /** + * Asserts that an MDX expression depends upon a given list of dimensions. + */ + @Override +public void assertExprDependsOn( String expr, String hierList ) { + // Construct a query, and mine it for a parsed expression. + // Use a fresh connection, because some tests define their own dims. + final Connection connection = getConnection(); + final String queryString = + "WITH MEMBER [Measures].[Foo] AS " + + Util.singleQuoteString( expr ) + + " SELECT FROM [Sales]"; + final Query query = connection.parseQuery( queryString ); + query.resolve(); + final Formula formula = query.getFormulas()[ 0 ]; + final Exp expression = formula.getExpression(); + + // Build a list of the dimensions which the expression depends upon, + // and check that it is as expected. + checkDependsOn( query, expression, hierList, true ); + } + + private void checkDependsOn( + final Query query, + final Exp expression, + String expectedHierList, + final boolean scalar ) { + final Calc calc = + query.compileExpression( + expression, + scalar, + scalar ? null : ResultStyle.ITERABLE ); + final List hierarchies = + ( (RolapCube) query.getCube() ).getHierarchies(); + StringBuilder buf = new StringBuilder( "{" ); + int dependCount = 0; + for ( Hierarchy hierarchy : hierarchies ) { + if ( calc.dependsOn( hierarchy ) ) { + if ( dependCount++ > 0 ) { + buf.append( ", " ); + } + buf.append( hierarchy.getUniqueName() ); + } + } + buf.append( "}" ); + String actualHierList = buf.toString(); + Assert.assertEquals( expectedHierList, actualHierList ); + } + + /** + * Creates a TestContext which is based on a variant of the FoodMart schema, which parameter, cube, named set, and + * user-defined function definitions added. + * + * @param parameterDefs Parameter definitions. If not null, the string is is inserted into the schema XML in the + * appropriate place for parameter definitions. + * @param cubeDefs Cube definition(s). If not null, the string is is inserted into the schema XML in the + * appropriate place for cube definitions. + * @param virtualCubeDefs Definitions of virtual cubes. If not null, the string is inserted into the schema XML in the + * appropriate place for virtual cube definitions. + * @param namedSetDefs Definitions of named sets. If not null, the string is inserted into the schema XML in the + * appropriate place for named set definitions. + * @param udfDefs Definitions of user-defined functions. If not null, the string is inserted into the schema + * XML in the appropriate place for UDF definitions. + * @param roleDefs Definitions of roles + * @return TestContext which reads from a slightly different hymnbook + */ + public final TestContext create( + final String parameterDefs, + final String cubeDefs, + final String virtualCubeDefs, + final String namedSetDefs, + final String udfDefs, + final String roleDefs ) { + final String schema = getSchema( + parameterDefs, cubeDefs, virtualCubeDefs, namedSetDefs, + udfDefs, roleDefs ); + return withSchema( schema ); + } + + /** + * Creates a TestContext which contains the given schema text. + * + * @param schema XML schema content + * @return TestContext which contains the given schema + */ + public final TestContext withSchema( final String schema ) { + final Util.PropertyList properties = getConnectionProperties().clone(); + properties.put( + RolapConnectionProperties.CatalogContent.name(), + schema ); + return withProperties( properties ); + } + + /** + * Creates a TestContext which is like this one but uses the given connection properties. + * + * @param properties Connection properties + * @return TestContext which contains the given properties + */ + public TestContext withProperties( final Util.PropertyList properties ) { + return new DelegatingTestContext( this ) { + public Util.PropertyList getConnectionProperties() { + return properties; + } + }; + } + + /** + * Creates a TestContext, adding hierarchy definitions to a cube definition. + * + * @param cubeName Name of a cube in the schema (cube must exist) + * @param dimensionDefs String defining dimensions, or null + * @return TestContext with modified cube defn + */ + public final TestContext createSubstitutingCube( + final String cubeName, + final String dimensionDefs ) { + return createSubstitutingCube( cubeName, dimensionDefs, null ); + } + + /** + * Creates a TestContext, adding hierarchy and calculated member definitions to a cube definition. + * + * @param cubeName Name of a cube in the schema (cube must exist) + * @param dimensionDefs String defining dimensions, or null + * @param memberDefs String defining calculated members, or null + * @return TestContext with modified cube defn + */ + public final TestContext createSubstitutingCube( + final String cubeName, + final String dimensionDefs, + final String memberDefs ) { + return createSubstitutingCube( + cubeName, dimensionDefs, null, memberDefs, null ); + } + + + /** + * Creates a TestContext, adding hierarchy and calculated member definitions to a cube definition. + * + * @param cubeName Name of a cube in the schema (cube must exist) + * @param dimensionDefs String defining dimensions, or null + * @param measureDefs String defining measures, or null + * @param memberDefs String defining calculated members, or null + * @param namedSetDefs String defining named set definitions, or null + * @return TestContext with modified cube defn + */ + public final TestContext createSubstitutingCube( + final String cubeName, + final String dimensionDefs, + final String measureDefs, + final String memberDefs, + final String namedSetDefs ) { + final String schema = + substituteSchema( + getRawFoodMartSchema(), + cubeName, dimensionDefs, + measureDefs, memberDefs, namedSetDefs, null ); + return withSchema( schema ); + } + + /** + * Overload that allows swapping the defaultMeasure. + */ + public final TestContext createSubstitutingCube( + final String cubeName, + final String dimensionDefs, + final String measureDefs, + final String memberDefs, + final String namedSetDefs, + final String defaultMeasure ) { + final String schema = + substituteSchema( + getRawFoodMartSchema(), + cubeName, dimensionDefs, + measureDefs, memberDefs, namedSetDefs, + defaultMeasure ); + return withSchema( schema ); + } + + + /** + * Returns a TestContext similar to this one, but using the given role. + * + * @param roleName Role name + * @return Test context with the given role + */ + public final TestContext withRole( final String roleName ) { + final Util.PropertyList properties = getConnectionProperties().clone(); + properties.put( + RolapConnectionProperties.Role.name(), + roleName ); + return new DelegatingTestContext( this ) { + public Util.PropertyList getConnectionProperties() { + return properties; + } + }; + } + + /** + * Returns a TestContext similar to this one, but using the given cube as default for tests such as {@link + * #assertExprReturns(String, String)}. + * + * @param cubeName Cube name + * @return Test context with the given default cube + */ + public final TestContext withCube( final String cubeName ) { + return new DelegatingTestContext( this ) { + public String getDefaultCubeName() { + return cubeName; + } + }; + } + + /** + * Returns a {@link FoodmartTestContextImpl} similar to this one, but which uses a given connection. + * + * @param connection Connection + * @return Test context which uses the given connection + */ + public final TestContext withConnection( final Connection connection ) { + return new DelegatingTestContext( this ) { + public Connection getConnection() { + return connection; + } + + @Override + public void close() { + connection.close(); + } + }; + } + + /** + * Generates a string containing all dimensions except those given. Useful as an argument to {@link + * #assertExprDependsOn(String, String)}. + * + * @return string containing all dimensions except those given + */ + public static String allHiersExcept( String... hiers ) { + for ( String hier : hiers ) { + assert contains( AllHiers, hier ) : "unknown hierarchy " + hier; + } + StringBuilder buf = new StringBuilder( "{" ); + int j = 0; + for ( String hier : AllHiers ) { + if ( !contains( hiers, hier ) ) { + if ( j++ > 0 ) { + buf.append( ", " ); + } + buf.append( hier ); + } + } + buf.append( "}" ); + return buf.toString(); + } + + public static boolean contains( String[] a, String s ) { + for ( String anA : a ) { + if ( anA.equals( s ) ) { + return true; + } + } + return false; + } + + public static String allHiers() { + return allHiersExcept(); + } + + /** + * Creates a FoodMart connection with "Ignore=true" and returns the list of warnings in the schema. + * + * @return Warnings encountered while loading schema + */ + @Override +public List getSchemaWarnings() { + final Util.PropertyList propertyList = + getConnectionProperties().clone(); + propertyList.put( + RolapConnectionProperties.Ignore.name(), + "true" ); + final Connection connection = + withProperties( propertyList ).getConnection(); + return connection.getSchema().getWarnings(); + } + + @Override +public OlapConnection getOlap4jConnection() throws SQLException { + try { + Class.forName( "mondrian.olap4j.MondrianOlap4jDriver" ); + } catch ( ClassNotFoundException e ) { + throw new RuntimeException( "Driver not found" ); + } + String connectString = getConnectString(); + if ( connectString.startsWith( "Provider=mondrian; " ) ) { + connectString = + connectString.substring( "Provider=mondrian; ".length() ); + } + final java.sql.Connection connection = + java.sql.DriverManager.getConnection( + "jdbc:mondrian:" + connectString ); + return ( (OlapWrapper) connection ).unwrap( OlapConnection.class ); + } + + /** + * Tests whether the database is valid. Allows tests that depend on optional databases to figure out whether to + * proceed. + * + * @return whether a database is present and correct + */ + @Override +public boolean databaseIsValid() { + try { + Connection connection = getConnection(); + String cubeName = getDefaultCubeName(); + if ( cubeName.indexOf( ' ' ) >= 0 ) { + cubeName = Util.quoteMdxIdentifier( cubeName ); + } + Query query = connection.parseQuery( "select from " + cubeName ); + Result result = connection.execute( query ); + Util.discard( result ); + connection.close(); + return true; + } catch ( RuntimeException e ) { + Util.discard( e ); + return false; + } + } + + public static String hierarchyName( String dimension, String hierarchy ) { + return MondrianProperties.instance().SsasCompatibleNaming.get() + ? "[" + dimension + "].[" + hierarchy + "]" + : ( hierarchy.equals( dimension ) + ? "[" + dimension + "]" + : "[" + dimension + "." + hierarchy + "]" ); + } + + public static String levelName( + String dimension, String hierarchy, String level ) { + return hierarchyName( dimension, hierarchy ) + ".[" + level + "]"; + } + + /** + * Returns count copies of a string. Format strings within string are substituted, per {@link + * java.lang.String#format}. + * + * @param count Number of copies + * @param format String template + * @return Multiple copies of a string + */ + public static String repeatString( + final int count, + String format ) { + final Formatter formatter = new Formatter(); + for ( int i = 0; i < count; i++ ) { + formatter.format( format, i ); + } + return formatter.toString(); + } + + //~ Inner classes ---------------------------------------------------------- + + public static class SnoopingSchemaProcessor + extends FilterDynamicSchemaProcessor { + public static final ThreadLocal THREAD_RESULT = + new ThreadLocal(); + + protected String filter( + String schemaUrl, + Util.PropertyList connectInfo, + InputStream stream ) throws Exception { + String catalogContent = + super.filter( schemaUrl, connectInfo, stream ); + THREAD_RESULT.set( catalogContent ); + return catalogContent; + } + } + + /** + * Schema processor that flags dimensions as high-cardinality if they appear in the list of values in the {@link + * MondrianProperties#TestHighCardinalityDimensionList} property. It's a convenient way to run the whole suite against + * high-cardinality dimensions without modifying FoodMart.xml. + */ + public static class HighCardDynamicSchemaProcessor + extends FilterDynamicSchemaProcessor { + protected String filter( + String schemaUrl, Util.PropertyList connectInfo, InputStream stream ) + throws Exception { + String s = super.filter( schemaUrl, connectInfo, stream ); + final String highCardDimensionList = + MondrianProperties.instance() + .TestHighCardinalityDimensionList.get(); + if ( highCardDimensionList != null + && !highCardDimensionList.equals( "" ) ) { + for ( String dimension : highCardDimensionList.split( "," ) ) { + final String match = + " e.g.
+ * stubAnonymousClasses("class mondrian.fun.Fun$21$1") + * results + * + * "class mondrian.fun.Fun$-anonymous-class-$-anonymous-class-" + * . + *
Within a Strings comparison
applying this to both compared Strings makes the comparison + * independent on anonymous class names. + *
+ */ + public static String stubAnonymousClasses( String str ) { + if ( !str.contains( "$" ) ) { + return str; + } + final String regex = + "(class mondrian(?:\\.\\w+)*(?:\\$(?:\\w+|-anonymous-class-))*?)(?:\\$\\d+)\\b"; + final String replacement = "$1\\$-anonymous-class-"; + Pattern p = Pattern.compile( regex ); + String str1 = p.matcher( str ).replaceAll( replacement ); + while ( !str.equals( str1 ) ) { + str = str1; + str1 = p.matcher( str ).replaceAll( replacement ); + } + return str1; + } + +} + +// End TestContext.java diff --git a/mondrian/src/test/java/mondrian/test/TestContext.java b/mondrian/src/test/java/mondrian/test/TestContext.java index a101e4837c..c6782bc447 100644 --- a/mondrian/src/test/java/mondrian/test/TestContext.java +++ b/mondrian/src/test/java/mondrian/test/TestContext.java @@ -1,2225 +1,265 @@ -/* -// This software is subject to the terms of the Eclipse Public License v1.0 -// Agreement, available at the following URL: -// http://www.eclipse.org/legal/epl-v10.html. -// You must accept the terms of that agreement to use this software. -// -// Copyright (C) 2002-2005 Julian Hyde -// Copyright (C) 2005-2020 Hitachi Vantara and others -// All Rights Reserved. -*/ package mondrian.test; - -import java.io.File; -import java.io.InputStream; import java.io.PrintWriter; -import java.io.StringWriter; -import java.lang.ref.SoftReference; -import java.lang.reflect.Proxy; -import java.net.MalformedURLException; -import java.net.URL; -import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; -import java.util.AbstractList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.Formatter; -import java.util.HashMap; -import java.util.Iterator; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; import java.util.regex.Pattern; -import javax.sql.DataSource; - import org.olap4j.CellSet; -import org.olap4j.CellSetAxis; import org.olap4j.OlapConnection; -import org.olap4j.OlapStatement; -import org.olap4j.OlapWrapper; -import org.olap4j.driver.xmla.XmlaOlap4jDriver; -import org.olap4j.impl.CoordinateIterator; -import org.olap4j.layout.TraditionalCellSetFormatter; -import junit.framework.Assert; import junit.framework.ComparisonFailure; -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; -import mondrian.calc.Calc; -import mondrian.calc.CalcWriter; -import mondrian.calc.ResultStyle; import mondrian.olap.Axis; import mondrian.olap.CacheControl; import mondrian.olap.Cell; import mondrian.olap.Connection; -import mondrian.olap.DriverManager; -import mondrian.olap.Exp; -import mondrian.olap.Formula; -import mondrian.olap.Hierarchy; import mondrian.olap.Member; -import mondrian.olap.MondrianProperties; -import mondrian.olap.Position; -import mondrian.olap.Query; import mondrian.olap.Result; import mondrian.olap.Util; -import mondrian.olap.fun.FunUtil; -import mondrian.olap4j.MondrianInprocProxy; -import mondrian.resource.MondrianResource; -import mondrian.rolap.RolapConnectionProperties; -import mondrian.rolap.RolapCube; -import mondrian.rolap.RolapHierarchy; -import mondrian.rolap.RolapUtil; import mondrian.spi.Dialect; -import mondrian.spi.DialectManager; -import mondrian.spi.DynamicSchemaProcessor; -import mondrian.spi.impl.FilterDynamicSchemaProcessor; -import mondrian.util.DelegatingInvocationHandler; /** - * TestContext is a singleton class which contains the information - * necessary to run mondrian tests (otherwise we'd have to pass this information into the constructor of TestCases). - * - *

The singleton instance (retrieved via the {@link #instance()} method) - * contains a connection to the FoodMart database, and runs expressions in the context of the Sales cube. - * - *

Using the {@link DelegatingTestContext} subclass, you can create derived - * classes which use a different connection or a different cube. - * - * @author jhyde - * @since 29 March, 2002 + * TestContext is a class which contains the information + * necessary to run mondrian tests. */ -public class TestContext { - private static TestContext instance; // the singleton - private PrintWriter pw; - - private SoftReference connectionRef; - - private Dialect dialect; - - protected static final String nl = Util.nl; - private static final String indent = " "; - private static final String lineBreak = "\"," + nl + "\""; - private static final String lineBreak2 = "\\\\n\"" + nl + indent + "+ \""; - private static final Pattern LineBreakPattern = - Pattern.compile( "\r\n|\r|\n" ); - private static final Pattern TabPattern = Pattern.compile( "\t" ); - private static final String[] AllHiers = { - "[Measures]", - "[Store]", - "[Store Size in SQFT]", - "[Store Type]", - "[Time]", - MondrianProperties.instance().SsasCompatibleNaming.get() ? "[Time].[Weekly]" : "[Time.Weekly]", - "[Product]", - "[Promotion Media]", - "[Promotions]", - "[Customers]", - "[Education Level]", - "[Gender]", - "[Marital Status]", - "[Yearly Income]" - }; - private static String unadulteratedFoodMartSchema; - - /** - * Retrieves the singleton (instantiating if necessary). - */ - public static synchronized TestContext instance() { - if ( instance == null ) { - instance = new TestContext(); - } - return instance; - } - - /** - * Creates a TestContext. - */ - protected TestContext() { - // Run all tests in the US locale, not the system default locale, - // because the results all assume the US locale. - MondrianResource.setThreadLocale( Locale.US ); - - - - this.pw = new PrintWriter( System.out, true ); - } - - /** - * Returns the connect string by which the unit tests can talk to the FoodMart database. - * - *

In the base class, the result is the same as the static method - * {@link #getDefaultConnectString}. If a derived class overrides {@link #getConnectionProperties()}, the result of - * this method will change also. - */ - public final String getConnectString() { - return getConnectionProperties().toString(); - } - - /** - * Constructs a connect string by which the unit tests can talk to the FoodMart database. - *

- * The algorithm is as follows:

    - *
  • Starts with {@link MondrianProperties#TestConnectString}, if it is - * set.
  • - *
  • If {@link MondrianProperties#FoodmartJdbcURL} is set, this - * overrides the Jdbc property.
  • - *
  • If the catalog URL is unset or invalid, it assumes that - * we are at the root of the source tree, and references - * demo/FoodMart.xml
  • . - *
- */ - public static String getDefaultConnectString() { - String connectString = - MondrianProperties.instance().TestConnectString.get(); - final Util.PropertyList connectProperties; - if ( connectString == null || connectString.equals( "" ) ) { - connectProperties = new Util.PropertyList(); - connectProperties.put( "Provider", "mondrian" ); - } else { - connectProperties = Util.parseConnectString( connectString ); - } - String jdbcURL = MondrianProperties.instance().FoodmartJdbcURL.get(); - if ( jdbcURL != null ) { - connectProperties.put( "Jdbc", jdbcURL ); - } - String jdbcUser = MondrianProperties.instance().TestJdbcUser.get(); - if ( jdbcUser != null ) { - connectProperties.put( "JdbcUser", jdbcUser ); - } - String jdbcPassword = - MondrianProperties.instance().TestJdbcPassword.get(); - if ( jdbcPassword != null ) { - connectProperties.put( "JdbcPassword", jdbcPassword ); - } - - // Find the catalog. Use the URL specified in the connect string, if - // it is specified and is valid. Otherwise, reference FoodMart.xml - // assuming we are at the root of the source tree. - URL catalogURL = null; - String catalog = connectProperties.get( "catalog" ); - if ( catalog != null ) { - try { - catalogURL = new URL( catalog ); - } catch ( MalformedURLException e ) { - // ignore - } - } - if ( catalogURL == null ) { - // Works if we are running in root directory of source tree - File file = new File( "demo/FoodMart.xml" ); - if ( !file.exists() ) { - // Works if we are running in bin directory of runtime env - file = new File( "../demo/FoodMart.xml" ); - } - try { - catalogURL = Util.toURL( file ); - } catch ( MalformedURLException e ) { - throw new Error( e.getMessage() ); - } - } - connectProperties.put( "catalog", catalogURL.toString() ); - return connectProperties.toString(); - } - - public synchronized void flushSchemaCache() { - // it's pointless to flush the schema cache if we - // have a handle on the connection object already - getConnection().getCacheControl( null ).flushSchemaCache(); - } - - /** - * Returns the connection to run queries. - * - *

When invoked on the default TestContext instance, returns a connection - * to the FoodMart database. - */ - public synchronized Connection getConnection() { - if ( connectionRef != null ) { - Connection connection = connectionRef.get(); - if ( connection != null ) { - return connection; - } - } - final Connection connection = - DriverManager.getConnection( - getConnectionProperties(), - null, - null ); - connectionRef = new SoftReference( connection ); - return connection; - } - - /** - * Returns a connection to the FoodMart database with a dynamic schema processor and disables use of RolapSchema - * Pool. - */ - public TestContext withSchemaProcessor( - Class dynProcClass ) { - final Util.PropertyList properties = getConnectionProperties().clone(); - properties.put( - RolapConnectionProperties.DynamicSchemaProcessor.name(), - dynProcClass.getName() ); - properties.put( - RolapConnectionProperties.UseSchemaPool.name(), - "false" ); - return withProperties( properties ); - } - - /** - * Returns a {@link TestContext} similar to this one, but which uses a fresh connection. - * - * @return Test context which uses the a fresh connection - * @see #withSchemaPool(boolean) - */ - public final TestContext withFreshConnection() { - final Connection connection = withSchemaPool( false ).getConnection(); - return withConnection( connection ); - } - - public TestContext withSchemaPool( boolean usePool ) { - final Util.PropertyList properties = getConnectionProperties().clone(); - properties.put( - RolapConnectionProperties.UseSchemaPool.name(), - Boolean.toString( usePool ) ); - return withProperties( properties ); - } - - public Util.PropertyList getConnectionProperties() { - final Util.PropertyList propertyList = - Util.parseConnectString( getDefaultConnectString() ); - if ( MondrianProperties.instance().TestHighCardinalityDimensionList - .get() != null - && propertyList.get( - RolapConnectionProperties.DynamicSchemaProcessor.name() ) - == null ) { - propertyList.put( - RolapConnectionProperties.DynamicSchemaProcessor.name(), - HighCardDynamicSchemaProcessor.class.getName() ); - } - return propertyList; - } - - /** - * Returns a the XML of the current schema with added parameters and cube definitions. - */ - public String getSchema( - String parameterDefs, - String cubeDefs, - String virtualCubeDefs, - String namedSetDefs, - String udfDefs, - String roleDefs ) { - // First, get the unadulterated schema. - String s = getRawFoodMartSchema(); - - // Add parameter definitions, if specified. - if ( parameterDefs != null ) { - int i = s.indexOf( "" ); - s = s.substring( 0, i ) - + parameterDefs - + s.substring( i ); - } - - // Add cube definitions, if specified. - if ( cubeDefs != null ) { - int i = - s.indexOf( - "" ); - s = s.substring( 0, i ) - + cubeDefs - + s.substring( i ); - } - - // Add virtual cube definitions, if specified. - if ( virtualCubeDefs != null ) { - int i = s.indexOf( - "" ); - s = s.substring( 0, i ) - + virtualCubeDefs - + s.substring( i ); - } - - // Add named set definitions, if specified. Schema-level named sets - // occur after and and before elements. - if ( namedSetDefs != null ) { - int i = s.indexOf( "" ); - } - s = s.substring( 0, i ) - + namedSetDefs - + s.substring( i ); - } - - // Add definitions of roles, if specified. - if ( roleDefs != null ) { - int i = s.indexOf( "" ); - } - s = s.substring( 0, i ) - + roleDefs - + s.substring( i ); - } - - // Add definitions of user-defined functions, if specified. - if ( udfDefs != null ) { - int i = s.indexOf( "" ); - s = s.substring( 0, i ) - + udfDefs - + s.substring( i ); - } - return s; - } - - /** - * Returns the definition of the "FoodMart" schema as stored in {@code FoodMart.xml}. - * - * @return XML definition of the FoodMart schema - */ - public static String getRawFoodMartSchema() { - synchronized ( SnoopingSchemaProcessor.class ) { - if ( unadulteratedFoodMartSchema == null ) { - unadulteratedFoodMartSchema = instance().getRawSchema(); - } - } - return unadulteratedFoodMartSchema; - } - - /** - * Returns the definition of the schema. - * - * @return XML definition of the FoodMart schema - */ - public String getRawSchema() { - final Connection connection = - withSchemaProcessor( SnoopingSchemaProcessor.class ) - .getConnection(); - connection.close(); - String schema = SnoopingSchemaProcessor.THREAD_RESULT.get(); - Util.threadLocalRemove( SnoopingSchemaProcessor.THREAD_RESULT ); - return schema; - } - - /** - * Returns a the XML of the foodmart schema, adding dimension definitions to the definition of a given cube. - */ - private String substituteSchema( - String rawSchema, - String cubeName, - String dimensionDefs, - String measureDefs, - String memberDefs, - String namedSetDefs, - String defaultMeasure ) { - String s = rawSchema; - - // Search for the or element. - int h = s.indexOf( "", h ); - } - - // Add dimension definitions, if specified. - if ( dimensionDefs != null ) { - int i = s.indexOf( " end ) { - i = end; - } - s = s.substring( 0, i ) - + measureDefs - + s.substring( i ); - - // Same for VirtualCubeMeasure - if ( i == end ) { - i = s.indexOf( " end ) { - i = end; - } - s = s.substring( 0, i ) - + measureDefs - + s.substring( i ); - } - } - - // Add calculated member definitions, if specified. - if ( memberDefs != null ) { - int i = s.indexOf( " end ) { - i = end; - } - s = s.substring( 0, i ) - + memberDefs - + s.substring( i ); - } - - if ( namedSetDefs != null ) { - int i = s.indexOf( " end ) { - i = end; - } - s = s.substring( 0, i ) - + namedSetDefs - + s.substring( i ); - } - if ( defaultMeasure != null ) { - s = s.replaceFirst( - "(" + cubeName + ".*)defaultMeasure=\"[^\"]*\"", - "$1defaultMeasure=\"" + defaultMeasure + "\"" ); - } - - return s; - } - - /** - * Executes a query. - * - * @param queryString Query string - */ - public Result executeQuery( String queryString ) { - Connection connection = getConnection(); - queryString = upgradeQuery( queryString ); - Query query = connection.parseQuery( queryString ); - final Result result = connection.execute( query ); - - // If we're deep testing, check that we never return the dummy null - // value when cells are null. TestExpDependencies isn't the perfect - // switch to enable this, but it will do for now. - if ( MondrianProperties.instance().TestExpDependencies.booleanValue() ) { - assertResultValid( result ); - } - return result; - } - - public ResultSet executeStatement( String queryString ) throws SQLException { - OlapConnection connection = getOlap4jConnection(); - queryString = upgradeQuery( queryString ); - OlapStatement stmt = connection.createStatement(); - return stmt.executeQuery( queryString ); - } - - /** - * Executes a query using olap4j. - */ - public CellSet executeOlap4jQuery( String queryString ) throws SQLException { - OlapConnection connection = getOlap4jConnection(); - queryString = upgradeQuery( queryString ); - OlapStatement stmt = connection.createStatement(); - final CellSet cellSet = stmt.executeOlapQuery( queryString ); - - // If we're deep testing, check that we never return the dummy null - // value when cells are null. TestExpDependencies isn't the perfect - // switch to enable this, but it will do for now. - if ( MondrianProperties.instance().TestExpDependencies.booleanValue() ) { - assertCellSetValid( cellSet ); - } - return cellSet; - } - - public CellSet executeOlap4jXmlaQuery( String queryString ) - throws SQLException { - String schema = getConnectionProperties() - .get( RolapConnectionProperties.CatalogContent.name() ); - if ( schema == null ) { - schema = getRawSchema(); - } - // TODO: Need to better handle semicolons in schema content. - // Util.parseValue does not appear to allow escaping them. - schema = schema.replace( """, "" ).replace( ";", "" ); - - String Jdbc = getConnectionProperties() - .get( RolapConnectionProperties.Jdbc.name() ); - - String cookie = XmlaOlap4jDriver.nextCookie(); - Map catalogs = new HashMap(); - catalogs.put( "FoodMart", "" ); - XmlaOlap4jDriver.PROXY_MAP.put( - cookie, new MondrianInprocProxy( - catalogs, - "jdbc:mondrian:Server=http://whatever;" - + "Jdbc=" + Jdbc + ";TestProxyCookie=" - + cookie - + ";CatalogContent=" + schema ) ); - try { - Class.forName( "org.olap4j.driver.xmla.XmlaOlap4jDriver" ); - } catch ( ClassNotFoundException e ) { - throw new RuntimeException( "oops", e ); - } - Properties info = new Properties(); - info.setProperty( - XmlaOlap4jDriver.Property.CATALOG.name(), "FoodMart" ); - java.sql.Connection connection = java.sql.DriverManager.getConnection( - "jdbc:xmla:Server=http://whatever;Catalog=FoodMart;TestProxyCookie=" - + cookie, - info ); - OlapConnection olapConnection = - connection.unwrap( OlapConnection.class ); - OlapStatement statement = olapConnection.createStatement(); - return statement.executeOlapQuery( queryString ); - } - - - /** - * Checks that a {@link Result} is valid. - * - * @param result Query result - */ - private static void assertResultValid( Result result ) { - for ( Cell cell : cellIter( result ) ) { - final Object value = cell.getValue(); - - // Check that the dummy value used to represent null cells never - // leaks into the outside world. - Assert.assertNotSame( value, Util.nullValue ); - Assert.assertFalse( - value instanceof Number - && ( (Number) value ).doubleValue() == FunUtil.DoubleNull ); - - // Similarly empty values. - Assert.assertNotSame( value, Util.EmptyValue ); - Assert.assertFalse( - value instanceof Number - && ( (Number) value ).doubleValue() == FunUtil.DoubleEmpty ); - - // Cells should be null if and only if they are null or empty. - if ( cell.getValue() == null ) { - Assert.assertTrue( cell.isNull() ); - } else { - Assert.assertFalse( cell.isNull() ); - } - } - - // There should be no null members. - for ( Axis axis : result.getAxes() ) { - for ( Position position : axis.getPositions() ) { - for ( Member member : position ) { - Assert.assertNotNull( member ); - } - } - } - } - - /** - * Checks that a {@link CellSet} is valid. - * - * @param cellSet Cell set - */ - private static void assertCellSetValid( CellSet cellSet ) { - for ( org.olap4j.Cell cell : cellIter( cellSet ) ) { - final Object value = cell.getValue(); - - // Check that the dummy value used to represent null cells never - // leaks into the outside world. - Assert.assertNotSame( value, Util.nullValue ); - Assert.assertFalse( - value instanceof Number - && ( (Number) value ).doubleValue() == FunUtil.DoubleNull ); - - // Similarly empty values. - Assert.assertNotSame( value, Util.EmptyValue ); - Assert.assertFalse( - value instanceof Number - && ( (Number) value ).doubleValue() == FunUtil.DoubleEmpty ); - - // Cells should be null if and only if they are null or empty. - if ( cell.getValue() == null ) { - Assert.assertTrue( cell.isNull() ); - } else { - Assert.assertFalse( cell.isNull() ); - } - } - - // There should be no null members. - for ( CellSetAxis axis : cellSet.getAxes() ) { - for ( org.olap4j.Position position : axis.getPositions() ) { - for ( org.olap4j.metadata.Member member : position.getMembers() ) { - Assert.assertNotNull( member ); - } - } - } - } - - /** - * Returns an iterator over cells in a result. - */ - static Iterable cellIter( final Result result ) { - return new Iterable() { - public Iterator iterator() { - int[] axisDimensions = new int[ result.getAxes().length ]; - int k = 0; - for ( Axis axis : result.getAxes() ) { - axisDimensions[ k++ ] = axis.getPositions().size(); - } - final CoordinateIterator - coordIter = new CoordinateIterator( axisDimensions ); - return new Iterator() { - public boolean hasNext() { - return coordIter.hasNext(); - } - - public Cell next() { - final int[] ints = coordIter.next(); - return result.getCell( ints ); - } - - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - }; - } - - /** - * Returns an iterator over cells in an olap4j cell set. - */ - static Iterable cellIter( final CellSet cellSet ) { - return new Iterable() { - public Iterator iterator() { - int[] axisDimensions = new int[ cellSet.getAxes().size() ]; - int k = 0; - for ( CellSetAxis axis : cellSet.getAxes() ) { - axisDimensions[ k++ ] = axis.getPositions().size(); - } - final CoordinateIterator - coordIter = new CoordinateIterator( axisDimensions ); - return new Iterator() { - public boolean hasNext() { - return coordIter.hasNext(); - } - - public org.olap4j.Cell next() { - final int[] ints = coordIter.next(); - final List list = - new AbstractList() { - public Integer get( int index ) { - return ints[ index ]; - } - - public int size() { - return ints.length; - } - }; - return cellSet.getCell( - list ); - } - - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - }; - } - - /** - * Executes a query, and asserts that it throws an exception which contains the given pattern. - * - * @param queryString Query string - * @param pattern Pattern which exception must match - */ - public void assertQueryThrows( String queryString, String pattern ) { - Throwable throwable; - try { - Result result = executeQuery( queryString ); - Util.discard( result ); - throwable = null; - } catch ( Throwable e ) { - throwable = e; - } - checkThrowable( throwable, pattern ); - } - - /** - * Executes an expression, and asserts that it gives an error which contains a particular pattern. The error might - * occur during parsing, or might be contained within the cell value. - */ - public void assertExprThrows( String expression, String pattern ) { - Throwable throwable = null; - try { - String cubeName = getDefaultCubeName(); - if ( cubeName.indexOf( ' ' ) >= 0 ) { - cubeName = Util.quoteMdxIdentifier( cubeName ); - } - expression = Util.replace( expression, "'", "''" ); - Result result = executeQuery( - "with member [Measures].[Foo] as '" - + expression - + "' select {[Measures].[Foo]} on columns from " - + cubeName ); - Cell cell = result.getCell( new int[] { 0 } ); - if ( cell.isError() ) { - throwable = (Throwable) cell.getValue(); - } - } catch ( Throwable e ) { - throwable = e; - } - checkThrowable( throwable, pattern ); - } - - /** - * Returns the name of the default cube. - * - *

Tests which evaluate scalar expressions, such as - * {@link #assertExprReturns(String, String)}, generate queries against this cube. - * - * @return the name of the default cube - */ - public String getDefaultCubeName() { - return "Sales"; - } - - /** - * Executes the expression in the context of the cube indicated by - * cubeName, and returns the result as a Cell. - * - * @param expression The expression to evaluate - * @return Cell which is the result of the expression - */ - public Cell executeExprRaw( String expression ) { - final String queryString = generateExpression( expression ); - Result result = executeQuery( queryString ); - return result.getCell( new int[] { 0 } ); - } - - private String generateExpression( String expression ) { - String cubeName = getDefaultCubeName(); - if ( cubeName.indexOf( ' ' ) >= 0 ) { - cubeName = Util.quoteMdxIdentifier( cubeName ); - } - return - "with member [Measures].[Foo] as " - + Util.singleQuoteString( expression ) - + " select {[Measures].[Foo]} on columns from " + cubeName; - } - - /** - * Executes an expression and asserts that it returns a given result. - */ - public void assertExprReturns( String expression, String expected ) { - final Cell cell = executeExprRaw( expression ); - if ( expected == null ) { - expected = ""; // null values are formatted as empty string - } - assertEqualsVerbose( expected, cell.getFormattedValue() ); - } - - /** - * Asserts that an expression, with a given set of parameter bindings, returns a given result. - * - * @param expr Scalar MDX expression - * @param expected Expected result - * @param paramValues Array of parameter names and values - */ - public void assertParameterizedExprReturns( - String expr, - String expected, - Object... paramValues ) { - Connection connection = getConnection(); - String queryString = generateExpression( expr ); - Query query = connection.parseQuery( queryString ); - assert paramValues.length % 2 == 0; - for ( int i = 0; i < paramValues.length; ) { - final String paramName = (String) paramValues[ i++ ]; - final Object value = paramValues[ i++ ]; - query.setParameter( paramName, value ); - } - final Result result = connection.execute( query ); - final Cell cell = result.getCell( new int[] { 0 } ); - - if ( expected == null ) { - expected = ""; // null values are formatted as empty string - } - assertEqualsVerbose( expected, cell.getFormattedValue() ); - } - - /** - * Executes a query with a given expression on an axis, and asserts that it returns the expected string. - */ - public void assertAxisReturns( - String expression, - String expected ) { - Axis axis = executeAxis( expression ); - assertEqualsVerbose( - expected, - upgradeActual( toString( axis.getPositions() ) ) ); - } - - /** - * Massages the actual result of executing a query to handle differences in unique names betweeen old and new - * behavior. - * - *

Even though the new naming is not enabled by default, reference logs - * should be in terms of the new naming. - * - * @param actual Actual result - * @return Expected result massaged for backwards compatibility - * @see mondrian.olap.MondrianProperties#SsasCompatibleNaming - */ - public String upgradeActual( String actual ) { - if ( !MondrianProperties.instance().SsasCompatibleNaming.get() ) { - actual = Util.replace( - actual, - "[Time.Weekly]", - "[Time].[Weekly]" ); - actual = Util.replace( - actual, - "[All Time.Weeklys]", - "[All Weeklys]" ); - actual = Util.replace( - actual, - "Time.Weekly", - "Weekly" ); - - // for a few tests in SchemaTest - actual = Util.replace( - actual, - "[Store.MyHierarchy]", - "[Store].[MyHierarchy]" ); - actual = Util.replace( - actual, - "[All Store.MyHierarchys]", - "[All MyHierarchys]" ); - actual = Util.replace( - actual, - "[Store2].[All Store2s]", - "[Store2].[Store].[All Stores]" ); - actual = Util.replace( - actual, - "[Store Type 2.Store Type 2].[All Store Type 2.Store Type 2s]", - "[Store Type 2].[All Store Type 2s]" ); - actual = Util.replace( - actual, - "[TIME.CALENDAR]", - "[TIME].[CALENDAR]" ); - actual = Util.replace( - actual, - "true", - "1" ); - actual = Util.replace( - actual, - "80000.0000", - "80000" ); - } - return actual; - } - - /** - * Massages an MDX query to handle differences in unique names betweeen old and new behavior. - * - *

The main difference addressed is with level naming. The problem - * arises when dimension, hierarchy and level have the same name:

    - * - *
  • In old behavior, the [Gender].[Gender] represents the Gender level, - * and [Gender].[Gender].[Gender] is invalid. - * - *
  • In new behavior, [Gender].[Gender] represents the Gender hierarchy, - * and [Gender].[Gender].[Gender].members represents the Gender level. - *

- * - *

So, {@code upgradeQuery("[Gender]")} returns - * "[Gender].[Gender]" for old behavior, "[Gender].[Gender].[Gender]" for new behavior.

- * - * @param queryString Original query - * @return Massaged query for backwards compatibility - * @see mondrian.olap.MondrianProperties#SsasCompatibleNaming - */ - public String upgradeQuery( String queryString ) { - if ( MondrianProperties.instance().SsasCompatibleNaming.get() ) { - String[] names = { - "[Gender]", - "[Education Level]", - "[Marital Status]", - "[Store Type]", - "[Yearly Income]", - }; - for ( String name : names ) { - queryString = Util.replace( - queryString, - name + "." + name, - name + "." + name + "." + name ); - } - queryString = Util.replace( - queryString, - "[Time.Weekly].[All Time.Weeklys]", - "[Time].[Weekly].[All Weeklys]" ); - } - return queryString; - } - - /** - * Compiles a scalar expression in the context of the default cube. - * - * @param expression The expression to evaluate - * @param scalar Whether the expression is scalar - * @return String form of the program - */ - public String compileExpression( String expression, final boolean scalar ) { - String cubeName = getDefaultCubeName(); - if ( cubeName.indexOf( ' ' ) >= 0 ) { - cubeName = Util.quoteMdxIdentifier( cubeName ); - } - final String queryString; - if ( scalar ) { - queryString = - "with member [Measures].[Foo] as " - + Util.singleQuoteString( expression ) - + " select {[Measures].[Foo]} on columns from " + cubeName; - } else { - queryString = - "SELECT {" + expression + "} ON COLUMNS FROM " + cubeName; - } - Connection connection = getConnection(); - Query query = connection.parseQuery( queryString ); - final Exp exp; - if ( scalar ) { - exp = query.getFormulas()[ 0 ].getExpression(); - } else { - exp = query.getAxes()[ 0 ].getSet(); - } - final Calc calc = query.compileExpression( exp, scalar, null ); - final StringWriter sw = new StringWriter(); - final PrintWriter pw = new PrintWriter( sw ); - final CalcWriter calcWriter = new CalcWriter( pw, false ); - calc.accept( calcWriter ); - pw.flush(); - return sw.toString(); - } - - /** - * Executes a set expression which is expected to return 0 or 1 members. It is an error if the expression returns - * tuples (as opposed to members), or if it returns two or more members. - * - * @param expression Expression string - * @return Null if axis returns the empty set, member if axis returns one member. Throws otherwise. - */ - public Member executeSingletonAxis( String expression ) { - final String cubeName = getDefaultCubeName(); - Result result = executeQuery( - "select {" + expression + "} on columns from " + cubeName ); - Axis axis = result.getAxes()[ 0 ]; - switch ( axis.getPositions().size() ) { - case 0: - // The mdx "{...}" operator eliminates null members (that is, - // members for which member.isNull() is true). So if "expression" - // yielded just the null member, the array will be empty. - return null; - case 1: - // Java nulls should never happen during expression evaluation. - Position position = axis.getPositions().get( 0 ); - Util.assertTrue( position.size() == 1 ); - Member member = position.get( 0 ); - Util.assertTrue( member != null ); - return member; - default: - throw Util.newInternal( - "expression " + expression - + " yielded " + axis.getPositions().size() + " positions" ); - } - } - - /** - * Executes a query with a given expression on an axis, and returns the whole axis. - */ - public Axis executeAxis( String expression ) { - Result result = executeQuery( - "select {" + expression - + "} on columns from " + getDefaultCubeName() ); - return result.getAxes()[ 0 ]; - } - - /** - * Executes a query with a given expression on an axis, and asserts that it throws an error which matches a particular - * pattern. The expression is evaulated against the default cube. - */ - public void assertAxisThrows( - String expression, - String pattern ) { - Throwable throwable = null; - Connection connection = getConnection(); - try { - final String cubeName = getDefaultCubeName(); - final String queryString = - "select {" + expression + "} on columns from " + cubeName; - Query query = connection.parseQuery( queryString ); - connection.execute( query ); - } catch ( Throwable e ) { - throwable = e; - } - checkThrowable( throwable, pattern ); - } - - public static void checkThrowable( Throwable throwable, String pattern ) { - if ( throwable == null ) { - Assert.fail( "query did not yield an exception" ); - } - String stackTrace = getStackTrace( throwable ); - if ( stackTrace.indexOf( pattern ) < 0 ) { - Assert.fail( - "query's error does not match pattern '" + pattern - + "'; error is [" + stackTrace + "]" ); - } - } +public interface TestContext { - /** - * Returns the output writer. - */ - public PrintWriter getWriter() { - return pw; - } - - /** - * Executes a query and checks that the result is a given string. - */ - public void assertQueryReturns( String query, String desiredResult ) { - Result result = executeQuery( query ); - String resultString = toString( result ); - if ( desiredResult != null ) { - assertEqualsVerbose( - desiredResult, - upgradeActual( resultString ) ); - } - } - - /** - * Executes a query and checks that the result is a given string, displaying a message if result does not match - * desiredResult. - */ - public void assertQueryReturns( - String message, String query, String desiredResult ) { - Result result = executeQuery( query ); - String resultString = toString( result ); - if ( desiredResult != null ) { - assertEqualsVerbose( - desiredResult, - upgradeActual( resultString ), - true, message ); - } - } - - - /** - * Executes a very simple query. - * - *

This forces the schema to be loaded and performs a basic sanity check. - * If this is a negative schema test, causes schema validation errors to be thrown. - */ - public void assertSimpleQuery() { - assertQueryReturns( - "select from [Sales]", - "Axis #0:\n" - + "{}\n" - + "266,773" ); - } - - /** - * Checks that an actual string matches an expected string. - * - *

If they do not, throws a {@link junit.framework.ComparisonFailure} and - * prints the difference, including the actual string as an easily pasted Java string literal. - */ - public static void assertEqualsVerbose( - String expected, - String actual ) { - assertEqualsVerbose( expected, actual, true, null ); - } - - /** - * Checks that an actual string matches an expected string. - * - *

If they do not, throws a {@link ComparisonFailure} and prints the - * difference, including the actual string as an easily pasted Java string literal. - * - * @param expected Expected string - * @param actual Actual string - * @param java Whether to generate actual string as a Java string literal if the values are not equal - * @param message Message to display, optional - */ - public static void assertEqualsVerbose( - String expected, - String actual, - boolean java, - String message ) { - assertEqualsVerbose( - fold( expected ), actual, java, message ); - } - - /** - * Checks that an actual string matches an expected string. - * - *

If they do not, throws a {@link ComparisonFailure} and prints the - * difference, including the actual string as an easily pasted Java string literal. - * - * @param safeExpected Expected string, where all line endings have been converted into platform-specific line - * endings - * @param actual Actual string - * @param java Whether to generate actual string as a Java string literal if the values are not equal - * @param message Message to display, optional - */ - public static void assertEqualsVerbose( - SafeString safeExpected, - String actual, - boolean java, - String message ) { - String expected = safeExpected == null ? null : safeExpected.s; - if ( ( expected == null ) && ( actual == null ) ) { - return; - } - if ( ( expected != null ) && expected.equals( actual ) ) { - return; - } - if ( message == null ) { - message = ""; - } else { - message += nl; - } - message += - "Expected:" + nl + expected + nl - + "Actual:" + nl + actual + nl; - if ( java ) { - message += "Actual java:" + nl + toJavaString( actual ) + nl; - } - throw new ComparisonFailure( message, expected, actual ); - } - - /** - * Checks that an actual string matches an expected string. Ignores the difference of anonymous class names in - * "mondrian...." package. - * - *

If they do not, throws a {@link junit.framework.ComparisonFailure} and - * prints the difference, including the actual string as an easily pasted Java string literal. - */ - public static void assertStubbedEqualsVerbose( - String expected, - String actual ) { - assertEqualsVerbose( - stubAnonymousClasses( expected ), - stubAnonymousClasses( actual ) ); - } - - private static String toJavaString( String s ) { - // Convert [string with "quotes" split - // across lines] - // into ["string with \"quotes\" split\n" - // + "across lines - // - s = Util.replace( s, "\"", "\\\"" ); - s = LineBreakPattern.matcher( s ).replaceAll( lineBreak2 ); - s = TabPattern.matcher( s ).replaceAll( "\\\\t" ); - s = "\"" + s + "\""; - String spurious = nl + indent + "+ \"\""; - if ( s.endsWith( spurious ) ) { - s = s.substring( 0, s.length() - spurious.length() ); - } - return s; - } - - /** - * Checks that an actual string matches an expected pattern. If they do not, throws a {@link ComparisonFailure} and - * prints the difference, including the actual string as an easily pasted Java string literal. - */ - public void assertMatchesVerbose( - Pattern expected, - String actual ) { - Util.assertPrecondition( expected != null, "expected != null" ); - if ( expected.matcher( actual ).matches() ) { - return; - } - String s = actual; - - // Convert [string with "quotes" split - // across lines] - // into ["string with \"quotes\" split" + nl + - // "across lines - // - s = Util.replace( s, "\"", "\\\"" ); - s = LineBreakPattern.matcher( s ).replaceAll( lineBreak ); - s = TabPattern.matcher( s ).replaceAll( "\\\\t" ); - s = "\"" + s + "\""; - final String spurious = " + " + nl + "\"\""; - if ( s.endsWith( spurious ) ) { - s = s.substring( 0, s.length() - spurious.length() ); - } - String message = - "Expected pattern:" + nl + expected + nl - + "Actual: " + nl + actual + nl - + "Actual java: " + nl + s + nl; - throw new ComparisonFailure( message, expected.pattern(), actual ); - } - - /** - * Converts a {@link Throwable} to a stack trace. - */ - public static String getStackTrace( Throwable e ) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter( sw ); - e.printStackTrace( pw ); - pw.flush(); - return sw.toString(); - } - - /** - * Converts a {@link mondrian.olap.Result} to text in traditional format. - * - *

For more exotic formats, see - * {@link org.olap4j.layout.CellSetFormatter}. - * - * @param result Query result - * @return Result as text - */ - public static String toString( Result result ) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter( sw ); - result.print( pw ); - pw.flush(); - return sw.toString(); - } - - /** - * Converts a {@link CellSet} to text in traditional format. - * - *

For more exotic formats, see - * {@link org.olap4j.layout.CellSetFormatter}. - * - * @param cellSet Query result - * @return Result as text - */ - public static String toString( CellSet cellSet ) { - final StringWriter sw = new StringWriter(); - new TraditionalCellSetFormatter().format( - cellSet, - new PrintWriter( sw ) ); - return sw.toString(); - } - - /** - * Returns a test context whose {@link #getOlap4jConnection()} method always returns the same connection object, and - * which has an active {@link org.olap4j.Scenario}, thus enabling writeback. - * - * @return Test context with active scenario - */ - public final TestContext withScenario() { - return new DelegatingTestContext( this ) { - OlapConnection connection; - - public OlapConnection getOlap4jConnection() throws SQLException { - if ( connection == null ) { - connection = super.getOlap4jConnection(); - connection.setScenario( - connection.createScenario() ); - } - return connection; - } - }; - } - - /** - * Converts a set of positions into a string. Useful if you want to check that an axis has the results you expected. - */ - public static String toString( List positions ) { - StringBuilder buf = new StringBuilder(); - int i = 0; - for ( Position position : positions ) { - if ( i > 0 ) { - buf.append( nl ); - } - if ( position.size() != 1 ) { - buf.append( "{" ); - } - for ( int j = 0; j < position.size(); j++ ) { - Member member = position.get( j ); - if ( j > 0 ) { - buf.append( ", " ); - } - buf.append( member.getUniqueName() ); - } - if ( position.size() != 1 ) { - buf.append( "}" ); - } - i++; - } - return buf.toString(); - } - - /** - * Makes a copy of a suite, filtering certain tests. - * - * @param suite Test suite - * @param testPattern Regular expression of name of tests to include - * @return copy of test suite - */ - public static TestSuite copySuite( - TestSuite suite, - Util.Functor1 testPattern ) { - TestSuite newSuite = new TestSuite( suite.getName() ); - //noinspection unchecked - for ( Test test : Collections.list( (Enumeration) suite.tests() ) ) { - if ( !testPattern.apply( test ) ) { - continue; - } - if ( test instanceof TestCase ) { - newSuite.addTest( test ); - } else if ( test instanceof TestSuite ) { - TestSuite subSuite = copySuite( (TestSuite) test, testPattern ); - if ( subSuite.countTestCases() > 0 ) { - newSuite.addTest( subSuite ); - } - } else { - // some other kind of test - newSuite.addTest( test ); - } - } - return newSuite; - } - - public void close() { - // nothing - } - - /** - * Returns a {@link CacheControl}. - */ - public CacheControl getCacheControl() { - return getConnection().getCacheControl( null ); - } - - /** - * Wrapper around a string that indicates that all line endings have been converted to platform-specific line - * endings. - * - * @see TestContext#fold - */ - public static class SafeString { - public final String s; - - private SafeString( String s ) { - this.s = s; - } - } - - /** - * Replaces line-endings in a string with the platform-dependent equivalent. If the input string already has - * platform-dependent line endings, no replacements are made. - * - * @param string String whose line endings are to be made platform- dependent. Typically these are constant "expected - * value" string expressions where the linefeed is represented as linefeed "\n", but sometimes this - * method will receive strings created dynamically where the line endings are already appropriate for - * the platform. - * @return String where all linefeeds have been converted to platform-specific (CR+LF on Windows, LF on Unix/Linux) - */ - public static SafeString fold( String string ) { - if ( string == null ) { - return null; - } - if ( nl.equals( "\n" ) || string.indexOf( nl ) != -1 ) { - return new SafeString( string ); - } - return new SafeString( Util.replace( string, "\n", nl ) ); - } - - /** - * Reverses the effect of {@link #fold}; converts platform-specific line endings in a string info linefeeds. - * - * @param string String where all linefeeds have been converted to platform-specific (CR+LF on Windows, LF on - * Unix/Linux) - * @return String where line endings are represented as linefeed "\n" - */ - public static String unfold( String string ) { - if ( !nl.equals( "\n" ) ) { - string = Util.replace( string, nl, "\n" ); - } - if ( string == null ) { - return null; - } else { - return string; - } - } - - public synchronized Dialect getDialect() { - if ( dialect == null ) { - dialect = getDialectInternal(); - } - return dialect; - } - - private Dialect getDialectInternal() { - DataSource dataSource = getConnection().getDataSource(); - return DialectManager.createDialect( dataSource, null ); - } - - /** - * Creates a dialect without using a connection. - * - * @param product Database product - * @return dialect of an required persuasion - */ - public static Dialect getFakeDialect( Dialect.DatabaseProduct product ) { - final DatabaseMetaData metaData = - (DatabaseMetaData) Proxy.newProxyInstance( - null, - new Class[] { DatabaseMetaData.class }, - new DatabaseMetaDataInvocationHandler( product ) ); - final java.sql.Connection connection = - (java.sql.Connection) Proxy.newProxyInstance( - null, - new Class[] { java.sql.Connection.class }, - new ConnectionInvocationHandler( metaData ) ); - final Dialect dialect = DialectManager.createDialect( null, connection ); - assert dialect.getDatabaseProduct() == product; - return dialect; - } - - /** - * Checks that expected SQL equals actual SQL. Performs some normalization on the actual SQL to compensate for - * differences between dialects. - */ - public void assertSqlEquals( - String expectedSql, - String actualSql, - int expectedRows ) { - // if the actual SQL isn't in the current dialect we have some - // problems... probably with the dialectize method - assertEqualsVerbose( actualSql, dialectize( actualSql ) ); - - String transformedExpectedSql = removeQuotes( dialectize( expectedSql ) ) - .replaceAll( "\r\n", "\n" ); - String transformedActualSql = removeQuotes( actualSql ) - .replaceAll( "\r\n", "\n" ); - Assert.assertEquals( transformedExpectedSql, transformedActualSql ); - - checkSqlAgainstDatasource( actualSql, expectedRows ); - } - - private static String removeQuotes( String actualSql ) { - String transformedActualSql = actualSql.replaceAll( "`", "" ); - transformedActualSql = transformedActualSql.replaceAll( "\"", "" ); - return transformedActualSql; - } - - /** - * Converts a SQL string into the current dialect. - * - *

This is not intended to be a general purpose method: it looks for - * specific patterns known to occur in tests, in particular "=as=" and "fname + ' ' + lname". - * - * @param sql SQL string in generic dialect - * @return SQL string converted into current dialect - */ - private String dialectize( String sql ) { - final String search = "fname \\+ ' ' \\+ lname"; - final Dialect dialect = getDialect(); - final Dialect.DatabaseProduct databaseProduct = - dialect.getDatabaseProduct(); - switch ( databaseProduct ) { - case MYSQL: - case MARIADB: - // Mysql would generate "CONCAT(...)" - sql = sql.replaceAll( - search, - "CONCAT(`customer`.`fname`, ' ', `customer`.`lname`)" ); - break; - case POSTGRESQL: - case ORACLE: - case LUCIDDB: - case TERADATA: - sql = sql.replaceAll( - search, - "`fname` || ' ' || `lname`" ); - break; - case DERBY: - sql = sql.replaceAll( - search, - "`customer`.`fullname`" ); - break; - case INGRES: - sql = sql.replaceAll( - search, - "fullname" ); - break; - case DB2: - case DB2_AS400: - case DB2_OLD_AS400: - sql = sql.replaceAll( - search, - "CONCAT(CONCAT(`customer`.`fname`, ' '), `customer`.`lname`)" ); - break; - } - - if ( dialect.getDatabaseProduct() == Dialect.DatabaseProduct.ORACLE ) { - // " + tableQualifier + " - sql = sql.replaceAll( " =as= ", " " ); - } else { - sql = sql.replaceAll( " =as= ", " as " ); - } - return sql; - } - - private void checkSqlAgainstDatasource( - String actualSql, - int expectedRows ) { - Util.PropertyList connectProperties = getConnectionProperties(); - - java.sql.Connection jdbcConn = null; - Statement stmt = null; - ResultSet rs = null; - - try { - String jdbcDrivers = - connectProperties.get( - RolapConnectionProperties.JdbcDrivers.name() ); - if ( jdbcDrivers != null ) { - RolapUtil.loadDrivers( jdbcDrivers ); - } - final String jdbcDriversProp = - MondrianProperties.instance().JdbcDrivers.get(); - RolapUtil.loadDrivers( jdbcDriversProp ); - - jdbcConn = java.sql.DriverManager.getConnection( - connectProperties.get( RolapConnectionProperties.Jdbc.name() ), - connectProperties.get( - RolapConnectionProperties.JdbcUser.name() ), - connectProperties.get( - RolapConnectionProperties.JdbcPassword.name() ) ); - stmt = jdbcConn.createStatement(); - - if ( RolapUtil.SQL_LOGGER.isDebugEnabled() ) { - StringBuffer sqllog = new StringBuffer(); - sqllog.append( "mondrian.test.TestContext: executing sql [" ); - if ( actualSql.indexOf( '\n' ) >= 0 ) { - // SQL appears to be formatted as multiple lines. Make it - // start on its own line. - sqllog.append( "\n" ); - } - sqllog.append( actualSql ); - sqllog.append( ']' ); - RolapUtil.SQL_LOGGER.debug( sqllog.toString() ); - } - - long startTime = System.currentTimeMillis(); - rs = stmt.executeQuery( actualSql ); - long time = System.currentTimeMillis(); - final long execMs = time - startTime; - Util.addDatabaseTime( execMs ); - - RolapUtil.SQL_LOGGER.debug( ", exec " + execMs + " ms" ); - - int rows = 0; - while ( rs.next() ) { - rows++; - } - - Assert.assertEquals( "row count", expectedRows, rows ); - } catch ( SQLException e ) { - throw new RuntimeException( - "ERROR in SQL - invalid for database: " - + connectProperties.get( RolapConnectionProperties.Jdbc.name() ) - + "\n" + actualSql, - e ); - } finally { - try { - if ( rs != null ) { - rs.close(); - } - } catch ( Exception e1 ) { - // ignore - } - try { - if ( stmt != null ) { - stmt.close(); - } - } catch ( Exception e1 ) { - // ignore - } - try { - if ( jdbcConn != null ) { - jdbcConn.close(); - } - } catch ( Exception e1 ) { - // ignore - } - } - } - - /** - * Asserts that an MDX set-valued expression depends upon a given list of dimensions. - */ - public void assertSetExprDependsOn( String expr, String dimList ) { - // Construct a query, and mine it for a parsed expression. - // Use a fresh connection, because some tests define their own dims. - final Connection connection = getConnection(); - final String queryString = - "SELECT {" + expr + "} ON COLUMNS FROM [Sales]"; - final Query query = connection.parseQuery( queryString ); - query.resolve(); - final Exp expression = query.getAxes()[ 0 ].getSet(); - - // Build a list of the dimensions which the expression depends upon, - // and check that it is as expected. - checkDependsOn( query, expression, dimList, false ); - } - - /** - * Asserts that an MDX member-valued depends upon a given list of dimensions. - */ - public void assertMemberExprDependsOn( String expr, String dimList ) { - assertSetExprDependsOn( "{" + expr + "}", dimList ); - } - - /** - * Asserts that an MDX expression depends upon a given list of dimensions. - */ - public void assertExprDependsOn( String expr, String hierList ) { - // Construct a query, and mine it for a parsed expression. - // Use a fresh connection, because some tests define their own dims. - final Connection connection = getConnection(); - final String queryString = - "WITH MEMBER [Measures].[Foo] AS " - + Util.singleQuoteString( expr ) - + " SELECT FROM [Sales]"; - final Query query = connection.parseQuery( queryString ); - query.resolve(); - final Formula formula = query.getFormulas()[ 0 ]; - final Exp expression = formula.getExpression(); - - // Build a list of the dimensions which the expression depends upon, - // and check that it is as expected. - checkDependsOn( query, expression, hierList, true ); - } - - private void checkDependsOn( - final Query query, - final Exp expression, - String expectedHierList, - final boolean scalar ) { - final Calc calc = - query.compileExpression( - expression, - scalar, - scalar ? null : ResultStyle.ITERABLE ); - final List hierarchies = - ( (RolapCube) query.getCube() ).getHierarchies(); - StringBuilder buf = new StringBuilder( "{" ); - int dependCount = 0; - for ( Hierarchy hierarchy : hierarchies ) { - if ( calc.dependsOn( hierarchy ) ) { - if ( dependCount++ > 0 ) { - buf.append( ", " ); - } - buf.append( hierarchy.getUniqueName() ); - } - } - buf.append( "}" ); - String actualHierList = buf.toString(); - Assert.assertEquals( expectedHierList, actualHierList ); - } - - /** - * Creates a TestContext which is based on a variant of the FoodMart schema, which parameter, cube, named set, and - * user-defined function definitions added. - * - * @param parameterDefs Parameter definitions. If not null, the string is is inserted into the schema XML in the - * appropriate place for parameter definitions. - * @param cubeDefs Cube definition(s). If not null, the string is is inserted into the schema XML in the - * appropriate place for cube definitions. - * @param virtualCubeDefs Definitions of virtual cubes. If not null, the string is inserted into the schema XML in the - * appropriate place for virtual cube definitions. - * @param namedSetDefs Definitions of named sets. If not null, the string is inserted into the schema XML in the - * appropriate place for named set definitions. - * @param udfDefs Definitions of user-defined functions. If not null, the string is inserted into the schema - * XML in the appropriate place for UDF definitions. - * @param roleDefs Definitions of roles - * @return TestContext which reads from a slightly different hymnbook - */ - public final TestContext create( - final String parameterDefs, - final String cubeDefs, - final String virtualCubeDefs, - final String namedSetDefs, - final String udfDefs, - final String roleDefs ) { - final String schema = getSchema( - parameterDefs, cubeDefs, virtualCubeDefs, namedSetDefs, - udfDefs, roleDefs ); - return withSchema( schema ); - } - - /** - * Creates a TestContext which contains the given schema text. - * - * @param schema XML schema content - * @return TestContext which contains the given schema - */ - public final TestContext withSchema( final String schema ) { - final Util.PropertyList properties = getConnectionProperties().clone(); - properties.put( - RolapConnectionProperties.CatalogContent.name(), - schema ); - return withProperties( properties ); - } - - /** - * Creates a TestContext which is like this one but uses the given connection properties. - * - * @param properties Connection properties - * @return TestContext which contains the given properties - */ - public TestContext withProperties( final Util.PropertyList properties ) { - return new DelegatingTestContext( this ) { - public Util.PropertyList getConnectionProperties() { - return properties; - } - }; - } - - /** - * Creates a TestContext, adding hierarchy definitions to a cube definition. - * - * @param cubeName Name of a cube in the schema (cube must exist) - * @param dimensionDefs String defining dimensions, or null - * @return TestContext with modified cube defn - */ - public final TestContext createSubstitutingCube( - final String cubeName, - final String dimensionDefs ) { - return createSubstitutingCube( cubeName, dimensionDefs, null ); - } - - /** - * Creates a TestContext, adding hierarchy and calculated member definitions to a cube definition. - * - * @param cubeName Name of a cube in the schema (cube must exist) - * @param dimensionDefs String defining dimensions, or null - * @param memberDefs String defining calculated members, or null - * @return TestContext with modified cube defn - */ - public final TestContext createSubstitutingCube( - final String cubeName, - final String dimensionDefs, - final String memberDefs ) { - return createSubstitutingCube( - cubeName, dimensionDefs, null, memberDefs, null ); - } + /** + * Returns the connect string by which the unit tests can talk to the database. + * + *

In the base class, the result is the same as the static method + * {@link #getDefaultConnectString}. If a derived class overrides {@link #getConnectionProperties()}, the result of + * this method will change also. + */ + String getConnectString(); + void flushSchemaCache(); - /** - * Creates a TestContext, adding hierarchy and calculated member definitions to a cube definition. - * - * @param cubeName Name of a cube in the schema (cube must exist) - * @param dimensionDefs String defining dimensions, or null - * @param measureDefs String defining measures, or null - * @param memberDefs String defining calculated members, or null - * @param namedSetDefs String defining named set definitions, or null - * @return TestContext with modified cube defn - */ - public final TestContext createSubstitutingCube( - final String cubeName, - final String dimensionDefs, - final String measureDefs, - final String memberDefs, - final String namedSetDefs ) { - final String schema = - substituteSchema( - getRawFoodMartSchema(), - cubeName, dimensionDefs, - measureDefs, memberDefs, namedSetDefs, null ); - return withSchema( schema ); - } + /** + * Returns the connection to run queries. + * + *

When invoked on the default TestContext instance, returns a connection + * to the database. + */ + Connection getConnection(); - /** - * Overload that allows swapping the defaultMeasure. - */ - public final TestContext createSubstitutingCube( - final String cubeName, - final String dimensionDefs, - final String measureDefs, - final String memberDefs, - final String namedSetDefs, - final String defaultMeasure ) { - final String schema = - substituteSchema( - getRawFoodMartSchema(), - cubeName, dimensionDefs, - measureDefs, memberDefs, namedSetDefs, - defaultMeasure ); - return withSchema( schema ); - } + Util.PropertyList getConnectionProperties(); + /** + * Returns the definition of the schema. + * + * @return XML definition of the given schema + */ + String getRawSchema(); - /** - * Returns a TestContext similar to this one, but using the given role. - * - * @param roleName Role name - * @return Test context with the given role - */ - public final TestContext withRole( final String roleName ) { - final Util.PropertyList properties = getConnectionProperties().clone(); - properties.put( - RolapConnectionProperties.Role.name(), - roleName ); - return new DelegatingTestContext( this ) { - public Util.PropertyList getConnectionProperties() { - return properties; - } - }; - } + /** + * Executes a query. + * + * @param queryString Query string + */ + Result executeQuery(String queryString); - /** - * Returns a TestContext similar to this one, but using the given cube as default for tests such as {@link - * #assertExprReturns(String, String)}. - * - * @param cubeName Cube name - * @return Test context with the given default cube - */ - public final TestContext withCube( final String cubeName ) { - return new DelegatingTestContext( this ) { - public String getDefaultCubeName() { - return cubeName; - } - }; - } + ResultSet executeStatement(String queryString) throws SQLException; - /** - * Returns a {@link TestContext} similar to this one, but which uses a given connection. - * - * @param connection Connection - * @return Test context which uses the given connection - */ - public final TestContext withConnection( final Connection connection ) { - return new DelegatingTestContext( this ) { - public Connection getConnection() { - return connection; - } + /** + * Executes a query using olap4j. + */ + CellSet executeOlap4jQuery(String queryString) throws SQLException; - @Override - public void close() { - connection.close(); - } - }; - } + CellSet executeOlap4jXmlaQuery(String queryString) throws SQLException; - /** - * Generates a string containing all dimensions except those given. Useful as an argument to {@link - * #assertExprDependsOn(String, String)}. - * - * @return string containing all dimensions except those given - */ - public static String allHiersExcept( String... hiers ) { - for ( String hier : hiers ) { - assert contains( AllHiers, hier ) : "unknown hierarchy " + hier; - } - StringBuilder buf = new StringBuilder( "{" ); - int j = 0; - for ( String hier : AllHiers ) { - if ( !contains( hiers, hier ) ) { - if ( j++ > 0 ) { - buf.append( ", " ); - } - buf.append( hier ); - } - } - buf.append( "}" ); - return buf.toString(); - } + /** + * Executes a query, and asserts that it throws an exception which contains the given pattern. + * + * @param queryString Query string + * @param pattern Pattern which exception must match + */ + void assertQueryThrows(String queryString, String pattern); - public static boolean contains( String[] a, String s ) { - for ( String anA : a ) { - if ( anA.equals( s ) ) { - return true; - } - } - return false; - } + /** + * Executes an expression, and asserts that it gives an error which contains a particular pattern. The error might + * occur during parsing, or might be contained within the cell value. + */ + void assertExprThrows(String expression, String pattern); - public static String allHiers() { - return allHiersExcept(); - } + /** + * Returns the name of the default cube. + * + *

Tests which evaluate scalar expressions, such as + * {@link #assertExprReturns(String, String)}, generate queries against this cube. + * + * @return the name of the default cube + */ + String getDefaultCubeName(); - /** - * Creates a FoodMart connection with "Ignore=true" and returns the list of warnings in the schema. - * - * @return Warnings encountered while loading schema - */ - public List getSchemaWarnings() { - final Util.PropertyList propertyList = - getConnectionProperties().clone(); - propertyList.put( - RolapConnectionProperties.Ignore.name(), - "true" ); - final Connection connection = - withProperties( propertyList ).getConnection(); - return connection.getSchema().getWarnings(); - } + /** + * Executes the expression in the context of the cube indicated by + * cubeName, and returns the result as a Cell. + * + * @param expression The expression to evaluate + * @return Cell which is the result of the expression + */ + Cell executeExprRaw(String expression); - public OlapConnection getOlap4jConnection() throws SQLException { - try { - Class.forName( "mondrian.olap4j.MondrianOlap4jDriver" ); - } catch ( ClassNotFoundException e ) { - throw new RuntimeException( "Driver not found" ); - } - String connectString = getConnectString(); - if ( connectString.startsWith( "Provider=mondrian; " ) ) { - connectString = - connectString.substring( "Provider=mondrian; ".length() ); - } - final java.sql.Connection connection = - java.sql.DriverManager.getConnection( - "jdbc:mondrian:" + connectString ); - return ( (OlapWrapper) connection ).unwrap( OlapConnection.class ); - } + /** + * Executes an expression and asserts that it returns a given result. + */ + void assertExprReturns(String expression, String expected); - /** - * Tests whether the database is valid. Allows tests that depend on optional databases to figure out whether to - * proceed. - * - * @return whether a database is present and correct - */ - public boolean databaseIsValid() { - try { - Connection connection = getConnection(); - String cubeName = getDefaultCubeName(); - if ( cubeName.indexOf( ' ' ) >= 0 ) { - cubeName = Util.quoteMdxIdentifier( cubeName ); - } - Query query = connection.parseQuery( "select from " + cubeName ); - Result result = connection.execute( query ); - Util.discard( result ); - connection.close(); - return true; - } catch ( RuntimeException e ) { - Util.discard( e ); - return false; - } - } + /** + * Asserts that an expression, with a given set of parameter bindings, returns a given result. + * + * @param expr Scalar MDX expression + * @param expected Expected result + * @param paramValues Array of parameter names and values + */ + void assertParameterizedExprReturns(String expr, String expected, Object... paramValues); - public static String hierarchyName( String dimension, String hierarchy ) { - return MondrianProperties.instance().SsasCompatibleNaming.get() - ? "[" + dimension + "].[" + hierarchy + "]" - : ( hierarchy.equals( dimension ) - ? "[" + dimension + "]" - : "[" + dimension + "." + hierarchy + "]" ); - } + /** + * Executes a query with a given expression on an axis, and asserts that it returns the expected string. + */ + void assertAxisReturns(String expression, String expected); - public static String levelName( - String dimension, String hierarchy, String level ) { - return hierarchyName( dimension, hierarchy ) + ".[" + level + "]"; - } + /** + * Massages the actual result of executing a query to handle differences in unique names betweeen old and new + * behavior. + * + *

Even though the new naming is not enabled by default, reference logs + * should be in terms of the new naming. + * + * @param actual Actual result + * @return Expected result massaged for backwards compatibility + * @see mondrian.olap.MondrianProperties#SsasCompatibleNaming + */ + String upgradeActual(String actual); - /** - * Returns count copies of a string. Format strings within string are substituted, per {@link - * java.lang.String#format}. - * - * @param count Number of copies - * @param format String template - * @return Multiple copies of a string - */ - public static String repeatString( - final int count, - String format ) { - final Formatter formatter = new Formatter(); - for ( int i = 0; i < count; i++ ) { - formatter.format( format, i ); - } - return formatter.toString(); - } + /** + * Massages an MDX query to handle differences in unique names betweeen old and new behavior. + * + *

The main difference addressed is with level naming. The problem + * arises when dimension, hierarchy and level have the same name:

    + * + *
  • In old behavior, the [Gender].[Gender] represents the Gender level, + * and [Gender].[Gender].[Gender] is invalid. + * + *
  • In new behavior, [Gender].[Gender] represents the Gender hierarchy, + * and [Gender].[Gender].[Gender].members represents the Gender level. + *

+ * + *

So, {@code upgradeQuery("[Gender]")} returns + * "[Gender].[Gender]" for old behavior, "[Gender].[Gender].[Gender]" for new behavior.

+ * + * @param queryString Original query + * @return Massaged query for backwards compatibility + * @see mondrian.olap.MondrianProperties#SsasCompatibleNaming + */ + String upgradeQuery(String queryString); - //~ Inner classes ---------------------------------------------------------- + /** + * Compiles a scalar expression in the context of the default cube. + * + * @param expression The expression to evaluate + * @param scalar Whether the expression is scalar + * @return String form of the program + */ + String compileExpression(String expression, boolean scalar); - public static class SnoopingSchemaProcessor - extends FilterDynamicSchemaProcessor { - public static final ThreadLocal THREAD_RESULT = - new ThreadLocal(); + /** + * Executes a set expression which is expected to return 0 or 1 members. It is an error if the expression returns + * tuples (as opposed to members), or if it returns two or more members. + * + * @param expression Expression string + * @return Null if axis returns the empty set, member if axis returns one member. Throws otherwise. + */ + Member executeSingletonAxis(String expression); - protected String filter( - String schemaUrl, - Util.PropertyList connectInfo, - InputStream stream ) throws Exception { - String catalogContent = - super.filter( schemaUrl, connectInfo, stream ); - THREAD_RESULT.set( catalogContent ); - return catalogContent; - } - } + /** + * Executes a query with a given expression on an axis, and returns the whole axis. + */ + Axis executeAxis(String expression); - /** - * Schema processor that flags dimensions as high-cardinality if they appear in the list of values in the {@link - * MondrianProperties#TestHighCardinalityDimensionList} property. It's a convenient way to run the whole suite against - * high-cardinality dimensions without modifying FoodMart.xml. - */ - public static class HighCardDynamicSchemaProcessor - extends FilterDynamicSchemaProcessor { - protected String filter( - String schemaUrl, Util.PropertyList connectInfo, InputStream stream ) - throws Exception { - String s = super.filter( schemaUrl, connectInfo, stream ); - final String highCardDimensionList = - MondrianProperties.instance() - .TestHighCardinalityDimensionList.get(); - if ( highCardDimensionList != null - && !highCardDimensionList.equals( "" ) ) { - for ( String dimension : highCardDimensionList.split( "," ) ) { - final String match = - "This forces the schema to be loaded and performs a basic sanity check. + * If this is a negative schema test, causes schema validation errors to be thrown. + */ + void assertSimpleQuery(); - // Public only because required for reflection to work. - @SuppressWarnings( "UnusedDeclaration" ) - public static class DatabaseMetaDataInvocationHandler - extends DelegatingInvocationHandler { - private final Dialect.DatabaseProduct product; + /** + * Checks that an actual string matches an expected pattern. If they do not, throws a {@link ComparisonFailure} and + * prints the difference, including the actual string as an easily pasted Java string literal. + */ + void assertMatchesVerbose(Pattern expected, String actual); - DatabaseMetaDataInvocationHandler( - Dialect.DatabaseProduct product ) { - this.product = product; - } + void close(); /** - * Proxy for {@link DatabaseMetaData#supportsResultSetConcurrency(int, int)}. - */ - public boolean supportsResultSetConcurrency( int type, int concurrency ) { - return false; - } + * Returns a {@link CacheControl}. + */ + CacheControl getCacheControl(); - /** - * Proxy for {@link DatabaseMetaData#getDatabaseProductName()}. - */ - public String getDatabaseProductName() { - switch ( product ) { - case GREENPLUM: - return "postgres greenplum"; - default: - return product.name(); - } - } + Dialect getDialect(); /** - * Proxy for {@link DatabaseMetaData#getIdentifierQuoteString()}. - */ - public String getIdentifierQuoteString() { - return "\""; - } + * Checks that expected SQL equals actual SQL. Performs some normalization on the actual SQL to compensate for + * differences between dialects. + */ + void assertSqlEquals(String expectedSql, String actualSql, int expectedRows); /** - * Proxy for {@link DatabaseMetaData#getDatabaseProductVersion()}. - */ - public String getDatabaseProductVersion() { - return "1.0"; - } + * Asserts that an MDX set-valued expression depends upon a given list of dimensions. + */ + void assertSetExprDependsOn(String expr, String dimList); /** - * Proxy for {@link DatabaseMetaData#isReadOnly()}. - */ - public boolean isReadOnly() { - return true; - } + * Asserts that an MDX member-valued depends upon a given list of dimensions. + */ + void assertMemberExprDependsOn(String expr, String dimList); /** - * Proxy for {@link DatabaseMetaData#getMaxColumnNameLength()}. - */ - public int getMaxColumnNameLength() { - return 30; - } + * Asserts that an MDX expression depends upon a given list of dimensions. + */ + void assertExprDependsOn(String expr, String hierList); /** - * Proxy for {@link DatabaseMetaData#getDriverName()}. - */ - public String getDriverName() { - switch ( product ) { - case GREENPLUM: - return "Mondrian fake dialect for Greenplum"; - default: - return "Mondrian fake dialect"; - } - } - } + * Creates a connection with "Ignore=true" and returns the list of warnings in the schema. + * + * @return Warnings encountered while loading schema + */ + List getSchemaWarnings(); - /** - * Replaces anonymous class names (/\$\d+/) with a stub "$-anonymous-class-" in constructions - * "class mondrian.rest.package.name.ClassName$InnerClassNames".
e.g.
- * stubAnonymousClasses("class mondrian.fun.Fun$21$1") - * results - * - * "class mondrian.fun.Fun$-anonymous-class-$-anonymous-class-" - * . - *
Within a Strings comparison
applying this to both compared Strings makes the comparison - * independent on anonymous class names. - *
- */ - public static String stubAnonymousClasses( String str ) { - if ( !str.contains( "$" ) ) { - return str; - } - final String regex = - "(class mondrian(?:\\.\\w+)*(?:\\$(?:\\w+|-anonymous-class-))*?)(?:\\$\\d+)\\b"; - final String replacement = "$1\\$-anonymous-class-"; - Pattern p = Pattern.compile( regex ); - String str1 = p.matcher( str ).replaceAll( replacement ); - while ( !str.equals( str1 ) ) { - str = str1; - str1 = p.matcher( str ).replaceAll( replacement ); - } - return str1; - } + OlapConnection getOlap4jConnection() throws SQLException; -} - -// End TestContext.java + /** + * Tests whether the database is valid. Allows tests that depend on optional databases to figure out whether to + * proceed. + * + * @return whether a database is present and correct + */ + boolean databaseIsValid(); + +} \ No newline at end of file