Skip to content

Commit 82c235a

Browse files
authored
Fix: insufficient test coverage in Snowflake (#959)
- Increase test coverage in snowflake - Make SnowflakeLogsConnector more testable - Add tests for Snowflake connector logic - Add a factory method for ConnectorArguments - Other fixes
1 parent c5f366b commit 82c235a

16 files changed

+703
-505
lines changed

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/ConnectorArguments.java

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import static com.google.common.base.MoreObjects.firstNonNull;
2020
import static com.google.common.collect.ImmutableList.toImmutableList;
21-
import static com.google.edwmigration.dumper.application.dumper.utils.OptionalUtils.optionallyWhen;
2221
import static java.time.temporal.ChronoUnit.DAYS;
2322
import static java.time.temporal.ChronoUnit.HOURS;
2423
import static java.util.Arrays.stream;
@@ -27,7 +26,6 @@
2726
import com.google.common.base.MoreObjects;
2827
import com.google.common.base.MoreObjects.ToStringHelper;
2928
import com.google.common.base.Predicates;
30-
import com.google.common.base.Strings;
3129
import com.google.common.collect.ImmutableList;
3230
import com.google.edwmigration.dumper.application.dumper.ZonedParser.DayOffset;
3331
import com.google.edwmigration.dumper.application.dumper.connector.Connector;
@@ -302,6 +300,14 @@ public class ConnectorArguments extends DefaultArguments {
302300
private final OptionSpec<Void> optionOutputContinue =
303301
parser.accepts("continue", "Continues writing a previous output file.");
304302

303+
/**
304+
* (Deprecated) earliest timestamp of logs to extract.
305+
*
306+
* <p>If the user specifies an earliest start time there will be extraneous empty dump files
307+
* because we always iterate over the full 7 trailing days; maybe it's worth preventing that in
308+
* the future. To do that, we should require getQueryLogEarliestTimestamp() to parse and return an
309+
* ISO instant, not a database-server-specific format.
310+
*/
305311
@Deprecated
306312
private final OptionSpec<String> optionQueryLogEarliestTimestamp =
307313
parser
@@ -590,10 +596,19 @@ public class ConnectorArguments extends DefaultArguments {
590596

591597
private ConnectorProperties connectorProperties;
592598

593-
private final PasswordReader passwordReader = new PasswordReader();
599+
private final PasswordReader passwordReader;
594600

595601
public ConnectorArguments(@Nonnull String... args) throws IOException {
602+
this(Arrays.asList(args), new PasswordReader());
603+
}
604+
605+
private ConnectorArguments(@Nonnull List<String> args, @Nonnull PasswordReader passwordReader) {
596606
super(args);
607+
this.passwordReader = passwordReader;
608+
}
609+
610+
public static ConnectorArguments create(@Nonnull List<String> args) {
611+
return new ConnectorArguments(args, new PasswordReader());
597612
}
598613

599614
@Override
@@ -751,10 +766,6 @@ public boolean isAssessment() {
751766
return getOptions().has(optionAssessment);
752767
}
753768

754-
private <T> Optional<T> optionAsOptional(OptionSpec<T> spec) {
755-
return optionallyWhen(getOptions().has(spec), () -> getOptions().valueOf(spec));
756-
}
757-
758769
@Nonnull
759770
public Predicate<String> getSchemaPredicate() {
760771
return toPredicate(getSchemata());
@@ -785,7 +796,11 @@ public String getUserOrFail() {
785796
*/
786797
@Nonnull
787798
public Optional<String> getPasswordIfFlagProvided() {
788-
return optionallyWhen(getOptions().has(optionPass), this::getPasswordOrPrompt);
799+
if (getOptions().has(optionPass)) {
800+
return Optional.of(getPasswordOrPrompt());
801+
} else {
802+
return Optional.empty();
803+
}
789804
}
790805

791806
@Nonnull
@@ -832,7 +847,15 @@ public List<String> getConfiguration() {
832847
}
833848

834849
public Optional<String> getOutputFile() {
835-
return optionAsOptional(optionOutput).filter(file -> !Strings.isNullOrEmpty(file));
850+
if (!getOptions().has(optionOutput)) {
851+
return Optional.empty();
852+
}
853+
String file = getOptions().valueOf(optionOutput);
854+
if (file == null || file.isEmpty()) {
855+
return Optional.empty();
856+
} else {
857+
return Optional.of(file);
858+
}
836859
}
837860

838861
public boolean isOutputContinue() {

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/DefaultArguments.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.io.PrintStream;
2424
import java.util.Arrays;
2525
import java.util.Comparator;
26+
import java.util.List;
2627
import java.util.Map;
2728
import java.util.Set;
2829
import java.util.TreeSet;
@@ -36,15 +37,10 @@
3637
import joptsimple.ValueConversionException;
3738
import joptsimple.ValueConverter;
3839
import org.anarres.jdiagnostics.ProductMetadata;
39-
import org.slf4j.Logger;
40-
import org.slf4j.LoggerFactory;
4140

4241
/** @author shevek */
4342
public class DefaultArguments {
4443

45-
@SuppressWarnings("UnusedVariable")
46-
private static final Logger logger = LoggerFactory.getLogger(DefaultArguments.class);
47-
4844
public static class BooleanValueConverter implements ValueConverter<Boolean> {
4945

5046
private final String[] V_TRUE = {"true", "t", "yes", "y", "1"};
@@ -96,9 +92,8 @@ public String valuePattern() {
9692
private final String[] args;
9793
private OptionSet options;
9894

99-
@SuppressWarnings("EI_EXPOSE_REP2")
100-
public DefaultArguments(@Nonnull String[] args) {
101-
this.args = args;
95+
DefaultArguments(@Nonnull List<String> args) {
96+
this.args = args.toArray(new String[0]);
10297
}
10398

10499
@Nonnull

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/snowflake/AbstractSnowflakeConnector.java

Lines changed: 9 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
*/
1717
package com.google.edwmigration.dumper.application.dumper.connector.snowflake;
1818

19+
import static com.google.common.base.CaseFormat.UPPER_CAMEL;
20+
import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
21+
1922
import com.google.common.base.CharMatcher;
2023
import com.google.common.base.Joiner;
21-
import com.google.common.collect.ImmutableList;
2224
import com.google.edwmigration.dumper.application.dumper.ConnectorArguments;
2325
import com.google.edwmigration.dumper.application.dumper.MetadataDumperUsageException;
2426
import com.google.edwmigration.dumper.application.dumper.annotations.RespectsArgumentDriver;
@@ -34,16 +36,12 @@
3436
import com.google.edwmigration.dumper.application.dumper.connector.Connector;
3537
import com.google.edwmigration.dumper.application.dumper.handle.Handle;
3638
import com.google.edwmigration.dumper.application.dumper.handle.JdbcHandle;
37-
import com.google.edwmigration.dumper.application.dumper.task.AbstractJdbcTask;
38-
import com.google.edwmigration.dumper.application.dumper.task.Summary;
39-
import com.google.edwmigration.dumper.application.dumper.task.Task;
4039
import java.sql.Driver;
4140
import java.sql.SQLException;
4241
import java.util.ArrayList;
4342
import java.util.List;
4443
import java.util.Properties;
4544
import javax.annotation.Nonnull;
46-
import javax.annotation.Nullable;
4745
import javax.sql.DataSource;
4846
import org.apache.commons.lang3.StringUtils;
4947
import org.springframework.jdbc.core.JdbcTemplate;
@@ -103,35 +101,14 @@ public Handle open(@Nonnull ConnectorArguments arguments)
103101

104102
@Override
105103
public final void validate(@Nonnull ConnectorArguments arguments) {
106-
ArrayList<String> messages = new ArrayList<>();
107-
MetadataDumperUsageException exception = null;
108-
109104
if (arguments.isPasswordFlagProvided() && arguments.isPrivateKeyFileProvided()) {
110105
String inconsistentAuth =
111106
"Private key authentication method can't be used together with user password. "
112107
+ "If the private key file is encrypted, please use --"
113108
+ ConnectorArguments.OPT_PRIVATE_KEY_PASSWORD
114109
+ " to specify the key password.";
115-
messages.add(inconsistentAuth);
116-
exception = new MetadataDumperUsageException(inconsistentAuth, messages);
117-
}
118-
119-
boolean hasDatabases = !arguments.getDatabases().isEmpty();
120-
if (arguments.isAssessment()
121-
&& hasDatabases
122-
&& arguments.getConnectorName().toLowerCase().equals("snowflake")) {
123-
String unsupportedFilter =
124-
"Trying to filter by database with the --"
125-
+ ConnectorArguments.OPT_ASSESSMENT
126-
+ " flag. This is unsupported in Assessment. Remove either the --"
127-
+ ConnectorArguments.OPT_ASSESSMENT
128-
+ " or the --"
129-
+ ConnectorArguments.OPT_DATABASE
130-
+ " flag.";
131-
messages.add(unsupportedFilter);
132-
exception = new MetadataDumperUsageException(unsupportedFilter, messages);
110+
throw new MetadataDumperUsageException(inconsistentAuth);
133111
}
134-
removeDuplicateMessageAndThrow(exception);
135112
validateForConnector(arguments);
136113
}
137114

@@ -144,15 +121,6 @@ public final void validate(@Nonnull ConnectorArguments arguments) {
144121
*/
145122
protected abstract void validateForConnector(@Nonnull ConnectorArguments arguments);
146123

147-
private static void removeDuplicateMessageAndThrow(
148-
@Nullable MetadataDumperUsageException exception) {
149-
if (exception != null) {
150-
List<String> messages = exception.getMessages();
151-
messages.remove(messages.size() - 1);
152-
throw exception;
153-
}
154-
}
155-
156124
private DataSource createUserPasswordDataSource(@Nonnull ConnectorArguments arguments, String url)
157125
throws SQLException {
158126
Driver driver =
@@ -205,23 +173,6 @@ private String getUrlFromArguments(@Nonnull ConnectorArguments arguments) {
205173
return buf.toString();
206174
}
207175

208-
final ImmutableList<Task<?>> getSqlTasks(
209-
@Nonnull SnowflakeInput inputSource,
210-
@Nonnull Class<? extends Enum<?>> header,
211-
@Nonnull String format,
212-
@Nonnull AbstractJdbcTask<Summary> schemaTask,
213-
@Nonnull AbstractJdbcTask<Summary> usageTask) {
214-
switch (inputSource) {
215-
case USAGE_THEN_SCHEMA_SOURCE:
216-
return ImmutableList.of(usageTask, schemaTask.onlyIfFailed(usageTask));
217-
case SCHEMA_ONLY_SOURCE:
218-
return ImmutableList.of(schemaTask);
219-
case USAGE_ONLY_SOURCE:
220-
return ImmutableList.of(usageTask);
221-
}
222-
throw new AssertionError();
223-
}
224-
225176
private void setCurrentDatabase(@Nonnull String databaseName, @Nonnull JdbcTemplate jdbcTemplate)
226177
throws MetadataDumperUsageException {
227178
String currentDatabase =
@@ -259,4 +210,9 @@ static String describeAsDelegate(Connector connector, String baseName) {
259210
String details = String.format("%8s[same options as '%s']\n", "", baseName);
260211
return summary + details;
261212
}
213+
214+
static String columnOf(Enum<?> enumValue) {
215+
String name = enumValue.name();
216+
return UPPER_CAMEL.to(UPPER_UNDERSCORE, name);
217+
}
262218
}

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/snowflake/SnowflakeAccountUsageMetadataConnector.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@
1616
*/
1717
package com.google.edwmigration.dumper.application.dumper.connector.snowflake;
1818

19+
import static com.google.edwmigration.dumper.application.dumper.ConnectorArguments.OPT_ASSESSMENT;
1920
import static com.google.edwmigration.dumper.application.dumper.connector.snowflake.SnowflakeInput.USAGE_ONLY_SOURCE;
2021

2122
import com.google.auto.service.AutoService;
23+
import com.google.edwmigration.dumper.application.dumper.ConnectorArguments;
24+
import com.google.edwmigration.dumper.application.dumper.MetadataDumperUsageException;
2225
import com.google.edwmigration.dumper.application.dumper.connector.Connector;
2326
import java.io.IOException;
2427
import javax.annotation.Nonnull;
28+
import javax.annotation.ParametersAreNonnullByDefault;
2529

2630
/** @author shevek */
2731
@AutoService(Connector.class)
32+
@ParametersAreNonnullByDefault
2833
public class SnowflakeAccountUsageMetadataConnector extends SnowflakeMetadataConnector {
2934

3035
public SnowflakeAccountUsageMetadataConnector() {
@@ -38,7 +43,15 @@ public String getDescription() {
3843
}
3944

4045
@Override
41-
public void printHelp(@Nonnull Appendable out) throws IOException {
46+
public void printHelp(Appendable out) throws IOException {
4247
out.append(AbstractSnowflakeConnector.describeAsDelegate(this, "snowflake"));
4348
}
49+
50+
@Override
51+
public final void validateForConnector(ConnectorArguments arguments) {
52+
if (arguments.isAssessment()) {
53+
String message = String.format("The --%s flag is not supported.", OPT_ASSESSMENT);
54+
throw new MetadataDumperUsageException(message);
55+
}
56+
}
4457
}

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/snowflake/SnowflakeInformationSchemaMetadataConnector.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@
1616
*/
1717
package com.google.edwmigration.dumper.application.dumper.connector.snowflake;
1818

19+
import static com.google.edwmigration.dumper.application.dumper.ConnectorArguments.OPT_ASSESSMENT;
1920
import static com.google.edwmigration.dumper.application.dumper.connector.snowflake.SnowflakeInput.SCHEMA_ONLY_SOURCE;
2021

2122
import com.google.auto.service.AutoService;
23+
import com.google.edwmigration.dumper.application.dumper.ConnectorArguments;
24+
import com.google.edwmigration.dumper.application.dumper.MetadataDumperUsageException;
2225
import com.google.edwmigration.dumper.application.dumper.connector.Connector;
2326
import java.io.IOException;
2427
import javax.annotation.Nonnull;
28+
import javax.annotation.ParametersAreNonnullByDefault;
2529

2630
/** @author shevek */
2731
@AutoService(Connector.class)
32+
@ParametersAreNonnullByDefault
2833
public class SnowflakeInformationSchemaMetadataConnector extends SnowflakeMetadataConnector {
2934

3035
public SnowflakeInformationSchemaMetadataConnector() {
@@ -41,4 +46,12 @@ public String getDescription() {
4146
public void printHelp(@Nonnull Appendable out) throws IOException {
4247
out.append(AbstractSnowflakeConnector.describeAsDelegate(this, "snowflake"));
4348
}
49+
50+
@Override
51+
public final void validateForConnector(ConnectorArguments arguments) {
52+
if (arguments.isAssessment()) {
53+
String message = String.format("The --%s flag is not supported.", OPT_ASSESSMENT);
54+
throw new MetadataDumperUsageException(message);
55+
}
56+
}
4457
}

dumper/app/src/main/java/com/google/edwmigration/dumper/application/dumper/connector/snowflake/SnowflakeInput.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
*/
1717
package com.google.edwmigration.dumper.application.dumper.connector.snowflake;
1818

19+
import com.google.common.collect.ImmutableList;
20+
import com.google.edwmigration.dumper.application.dumper.task.AbstractJdbcTask;
21+
import com.google.edwmigration.dumper.application.dumper.task.Task;
22+
import javax.annotation.Nonnull;
23+
import javax.annotation.ParametersAreNonnullByDefault;
24+
1925
/**
2026
* Represents a strategy of getting Snowflake data.
2127
*
@@ -29,11 +35,33 @@
2935
* https://docs.snowflake.net/manuals/user-guide/data-share-consumers.html You must: GRANT IMPORTED
3036
* PRIVILEGES ON DATABASE snowflake TO ROLE <SOMETHING>;
3137
*/
38+
@ParametersAreNonnullByDefault
3239
enum SnowflakeInput {
3340
/** Get data from ACCOUNT_USAGE contents, with a fallback to INFORMATION_SCHEMA. */
34-
USAGE_THEN_SCHEMA_SOURCE,
41+
USAGE_THEN_SCHEMA_SOURCE {
42+
@Override
43+
@Nonnull
44+
ImmutableList<Task<?>> sqlTasks(AbstractJdbcTask<?> schemaTask, AbstractJdbcTask<?> usageTask) {
45+
return ImmutableList.of(usageTask, schemaTask.onlyIfFailed(usageTask));
46+
}
47+
},
3548
/** Get data relying only on the contents of INFORMATION_SCHEMA */
36-
SCHEMA_ONLY_SOURCE,
49+
SCHEMA_ONLY_SOURCE {
50+
@Override
51+
@Nonnull
52+
ImmutableList<Task<?>> sqlTasks(AbstractJdbcTask<?> schemaTask, AbstractJdbcTask<?> usageTask) {
53+
return ImmutableList.of(schemaTask);
54+
}
55+
},
3756
/** Get data relying only on the contents of ACCOUNT_USAGE */
38-
USAGE_ONLY_SOURCE;
57+
USAGE_ONLY_SOURCE {
58+
@Override
59+
@Nonnull
60+
ImmutableList<Task<?>> sqlTasks(AbstractJdbcTask<?> schemaTask, AbstractJdbcTask<?> usageTask) {
61+
return ImmutableList.of(usageTask);
62+
}
63+
};
64+
65+
@Nonnull
66+
abstract ImmutableList<Task<?>> sqlTasks(AbstractJdbcTask<?> schema, AbstractJdbcTask<?> usage);
3967
}

0 commit comments

Comments
 (0)