Skip to content

ARROW-12500: [C++][Datasets] Ensure better test coverage of Dataset file formats - #10134

Closed
lidavidm wants to merge 8 commits into
apache:masterfrom
lidavidm:arrow-12500
Closed

lidavidm wants to merge 8 commits into
apache:masterfrom
lidavidm:arrow-12500

Conversation

@lidavidm

Copy link
Copy Markdown
Member

This unifies (most of) the tests across Parquet, Feather, and CSV (with carve-outs for particular cases). In particular, this means all formats are now tested in conjunction with async/sync and serial/threaded scanners. Also, a set of common file format tests were refactored out of the individual tests and centralized.


// Properly set conversion types even for non-materialized fields
// (since we're reading all of them anyways)
for (auto field : scan_options->dataset_schema->fields()) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unifying the tests led me to discover this, which I think inadvertently fixes a bug I ran into in ARROW-9697 that I hadn't figured out before. There, I found that for CSV fragments to_table(schema=..., columns=[]) would fail when to_table(schema=...) did not fail, because in the former case, the CSV reader would complain about an invalid cast, seemingly ignoring the schema set.

I think this isn't the best solution, however; we should actually be deriving include_columns.

@github-actions

Copy link
Copy Markdown

@westonpace westonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great. Getting rid of a lot of duplicate code. I still want to review the tests removed but here are some initial thoughts.

Comment thread cpp/src/arrow/dataset/file_csv.cc Outdated
if (column_names.find(field->name()) == column_names.end()) continue;
convert_options.column_types[field->name()] = field->type();
// Only read the requested columns
if (materialized_fields.find(field->name()) == materialized_fields.end()) continue;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty minor nit but maybe move this above convert_options.column_types... so that convert_options.column_types doesn't include columns that are not in convert_options.include_columns.

Comment thread cpp/src/arrow/dataset/file_csv_test.cc Outdated
namespace dataset {

class TestCsvFileFormat : public testing::TestWithParam<Compression::type> {
class ArrowCsvWriterMixin {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming nit: Not sure mixin is the right term here. When I think mixin I think multiple inheritance. Maybe Strategy? Also, why "ArrowCsv". Are there other CSV writers? Also, maybe there is a way to use composition here? Have an abstract method in FileFormatFixtureMixin that returns a FormatStrategy or something? I know "prefer composition over inheritance" but I don't know if there is a similar "prefer composition over template" guideline 😆

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally tried with abstract methods, but then because of the inheritance tree, you'd have to re-implement them in the AbcFileFormatTest and in the AbcFileFormatScanTest fixtures (I had issues when trying to just have the latter inherit the former). Probably there is some combination of superclasses and interfaces that'll make it work but at that point I decided a template was the lesser evil.

Comment thread cpp/src/arrow/dataset/test_util.h Outdated
void TestOpenFailureWithRelevantError(FileFormat* format, StatusCode code) {
std::shared_ptr<Buffer> buf = std::make_shared<Buffer>(util::string_view(""));
auto result = format->Inspect(FileSource(buf));
EXPECT_FALSE(result.ok());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These ok checks seem redundant given we are not testing Result/Status here and you test the code on the next line.

Comment thread cpp/src/arrow/dataset/test_util.h Outdated
}

// Shared test cases
void TestOpenFailureWithRelevantError(FileFormat* format, StatusCode code) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to TestInspectFailureWithRelevantError? There is no method named Open on FileFormat.

Comment thread cpp/src/arrow/dataset/file_csv_test.cc Outdated
@@ -199,18 +210,11 @@ TEST_P(TestCsvFileFormat, OpenFailureWithRelevantError) {
if (GetCompression() != Compression::type::UNCOMPRESSED) {
GTEST_SKIP() << "File source name is different with compression";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really a problem? Compression is just adding to the filename and we are testing with HasSubstr.

ASSERT_OK(SetProjection(opts_.get(), opts_->dataset_schema->field_names()));
}

void SetFilter(Expression filter) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR but I feel like there are a number of places where we have to do something like this to maintain implicit invariants. It might be nice to have a ScanOptions cleanup at some point. Change the name so it isn't Options, hide it from the public API, give it more methods (like SetFilter) and private state.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to ARROW-12311.

Comment thread cpp/src/arrow/dataset/test_util.h Outdated
ASSERT_OK_AND_ASSIGN(supported, format->IsSupported(*source));
EXPECT_EQ(supported, true);
}
std::shared_ptr<Buffer> TestWrite(FileFormat* format, std::shared_ptr<Schema> schema,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This is more helper function than test. Perhaps rename to WriteToBuffer? Also, we could create a TestWrite here that does...

  auto reader = GetRecordBatchReader(schema({field("f64", float64())}));
  auto source = GetFileSource(reader.get());
  auto written = WriteToBuffer(format_.get(), reader->schema());
  AssertBufferEqual(*written, *source->buffer());

...that should be universally applicable.

Comment thread cpp/src/arrow/dataset/test_util.h Outdated
auto dataset = std::make_shared<FragmentDataset>(schema, FragmentVector{fragment});
ScannerBuilder builder(dataset, opts_);
ARROW_EXPECT_OK(builder.UseAsync(GetParam().use_async));
ARROW_EXPECT_OK(builder.UseThreads(GetParam().use_threads));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably no need to set UseThreads here, that will not affect how the FileFormat is interacted with.

Comment thread cpp/src/arrow/dataset/test_util.h Outdated
ASSERT_EQ(row_count, GetParam().expected_rows());
}

void TestScanProjected(FileFormat* format) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If projection is applied by the scanner why are we testing it here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is testing whether each format will look at the projection in the ScanOptions and only return the necessary columns to fulfill the projection + filter later on. Notably, CSV didn't properly do that before.

}

// Scan the fragment through the scanner.
RecordBatchIterator Batches(std::shared_ptr<Fragment> fragment) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this add to FileFormat testing above and beyond ScanBatches? One could argue that PhysicalBatches is all you need.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I wanted to have coverage of each file type with the actual scanner (right now we test each format in isolation + the scanner in isolation). But that is overkill as you point out.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be easily convinced either way. I think it's also good to detect potential interaction bugs. I suppose the scanner could do various things with scan options (like the projection issue you mentioned) that aren't so easily exhaustively tested in formats.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On balance I think I'll refactor this since you plan to add fuller integration tests for datasets which is a better spot to catch more involved things. The test cases here I'll keep, but only to directly test Scan/ScanBatches.

@lidavidm

Copy link
Copy Markdown
Member Author

Ah, despite all the diffs, no tests should have been removed - only shuffled around or consolidated when they were the same exact test.

@westonpace westonpace left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have been clearer earlier. I just wanted to do a quick pass to make sure that no tests got missed in the refactor. But I can't see any so looks good. I don't think any of my change in this PR or the other are required.

auto sink = CreateOutputStream(pool);

ARROW_EXPECT_OK(WriteRecordBatchReader(reader, pool, sink));
// XXX the rest of the test may crash if this fails, since out will be nullptr

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor but could the method be changed to return Result so this comment can go away?

@lidavidm

Copy link
Copy Markdown
Member Author

Thanks for the quick review. I think everything's been addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants