$ python -m unittest discover -s tests -v
exit_code: 0
test_blank_record (test_split_csv.ChunkTests.test_blank_record) ... ok
test_crlf_input_keeps_fields_and_original_bytes (test_split_csv.ChunkTests.test_crlf_input_keeps_fields_and_original_bytes) ... ok
test_crlf_memo_preserves_exact_field_content (test_split_csv.ChunkTests.test_crlf_memo_preserves_exact_field_content) ... ok
test_duplicate_id_across_chunks (test_split_csv.ChunkTests.test_duplicate_id_across_chunks) ... ok
test_empty_file (test_split_csv.ChunkTests.test_empty_file) ... ok
test_empty_item (test_split_csv.ChunkTests.test_empty_item) ... ok
test_existing_dangling_symlink_preserved (test_split_csv.ChunkTests.test_existing_dangling_symlink_preserved) ... ok
test_existing_file_preserved (test_split_csv.ChunkTests.test_existing_file_preserved) ... ok
test_existing_folder_preserved (test_split_csv.ChunkTests.test_existing_folder_preserved) ... ok
test_existing_live_symlink_preserved (test_split_csv.ChunkTests.test_existing_live_symlink_preserved) ... ok
test_field_over_policy_limit_refused (test_split_csv.ChunkTests.test_field_over_policy_limit_refused) ... ok
test_formula_like_memo_remains_text_content (test_split_csv.ChunkTests.test_formula_like_memo_remains_text_content) ... ok
test_four_exact_multiple_eight (test_split_csv.ChunkTests.test_four_exact_multiple_eight) ... ok
test_four_four_two_and_exact_order_content (test_split_csv.ChunkTests.test_four_four_two_and_exact_order_content) ... ok
test_header_only_zero_chunks (test_split_csv.ChunkTests.test_header_only_zero_chunks) ... ok
test_invalid_utf8 (test_split_csv.ChunkTests.test_invalid_utf8) ... ok
test_isolated_cr_memo_preserves_exact_field_content (test_split_csv.ChunkTests.test_isolated_cr_memo_preserves_exact_field_content) ... ok
test_late_parse_failure_cleans_staged_chunks (test_split_csv.ChunkTests.test_late_parse_failure_cleans_staged_chunks) ... ok
test_max_one (test_split_csv.ChunkTests.test_max_one) ... ok
test_max_ten_no_trailing_empty (test_split_csv.ChunkTests.test_max_ten_no_trailing_empty) ... ok
test_missing_output_parent_refused (test_split_csv.ChunkTests.test_missing_output_parent_refused) ... ok
test_missing_source_refused (test_split_csv.ChunkTests.test_missing_source_refused) ... ok
test_negative_chunk_size (test_split_csv.ChunkTests.test_negative_chunk_size) ... ok
test_noncanonical_id (test_split_csv.ChunkTests.test_noncanonical_id) ... ok
test_noninteger_chunk_size (test_split_csv.ChunkTests.test_noninteger_chunk_size) ... ok
test_output_equals_source_preserved (test_split_csv.ChunkTests.test_output_equals_source_preserved) ... ok
test_output_parent_symlink_refused (test_split_csv.ChunkTests.test_output_parent_symlink_refused) ... ok
test_output_under_source_file_refused (test_split_csv.ChunkTests.test_output_under_source_file_refused) ... ok
test_quote_followed_by_text (test_split_csv.ChunkTests.test_quote_followed_by_text) ... ok
test_repeated_header_in_data (test_split_csv.ChunkTests.test_repeated_header_in_data) ... ok
test_rerun_refuses_and_new_name_works (test_split_csv.ChunkTests.test_rerun_refuses_and_new_name_works) ... ok
test_source_directory_refused (test_split_csv.ChunkTests.test_source_directory_refused) ... ok
test_source_symlink_refused (test_split_csv.ChunkTests.test_source_symlink_refused) ... ok
test_too_few_fields (test_split_csv.ChunkTests.test_too_few_fields) ... ok
test_too_many_fields (test_split_csv.ChunkTests.test_too_many_fields) ... ok
test_two_exact_multiple_ten (test_split_csv.ChunkTests.test_two_exact_multiple_ten) ... ok
test_unclosed_quote (test_split_csv.ChunkTests.test_unclosed_quote) ... ok
test_utf8_bom_policy (test_split_csv.ChunkTests.test_utf8_bom_policy) ... ok
test_verifier_detects_content_change (test_split_csv.ChunkTests.test_verifier_detects_content_change) ... ok
test_verifier_detects_duplicate_id (test_split_csv.ChunkTests.test_verifier_detects_duplicate_id) ... ok
test_verifier_detects_missing_chunk (test_split_csv.ChunkTests.test_verifier_detects_missing_chunk) ... ok
test_verifier_detects_reordering (test_split_csv.ChunkTests.test_verifier_detects_reordering) ... ok
test_verifier_detects_repeated_header (test_split_csv.ChunkTests.test_verifier_detects_repeated_header) ... ok
test_verifier_detects_source_bytes_change (test_split_csv.ChunkTests.test_verifier_detects_source_bytes_change) ... ok
test_verifier_detects_trailing_empty_chunk (test_split_csv.ChunkTests.test_verifier_detects_trailing_empty_chunk) ... ok
test_verifier_requires_summary (test_split_csv.ChunkTests.test_verifier_requires_summary) ... ok
test_wrong_header (test_split_csv.ChunkTests.test_wrong_header) ... ok
test_zero_chunk_size (test_split_csv.ChunkTests.test_zero_chunk_size) ... ok

----------------------------------------------------------------------
Ran 48 tests in 2.091s

OK

$ python split_csv.py split --source input/records.csv --output chunks4 --max-rows 4
exit_code: 0
{
  "rows": 10,
  "chunks": 3,
  "max_rows": 4,
  "rows_per_chunk": [
    4,
    4,
    2
  ],
  "headers_per_chunk": [
    1,
    1,
    1
  ],
  "duplicates": 0,
  "id_sequence_equal": true,
  "field_values_equal": true,
  "source_sha256": "961a45aa41dbead9203a56b1e144c5b17623fc81b742fdae351c2fb85c6d01e3"
}

$ python split_csv.py verify --source input/records.csv --output chunks4 --max-rows 4
exit_code: 0
{
  "rows": 10,
  "chunks": 3,
  "max_rows": 4,
  "rows_per_chunk": [
    4,
    4,
    2
  ],
  "headers_per_chunk": [
    1,
    1,
    1
  ],
  "duplicates": 0,
  "id_sequence_equal": true,
  "field_values_equal": true,
  "source_sha256": "961a45aa41dbead9203a56b1e144c5b17623fc81b742fdae351c2fb85c6d01e3"
}

$ python split_csv.py split --source input/records.csv --output chunks1 --max-rows 1
exit_code: 0
{
  "rows": 10,
  "chunks": 10,
  "max_rows": 1,
  "rows_per_chunk": [
    1,
    1,
    1,
    1,
    1,
    1,
    1,
    1,
    1,
    1
  ],
  "headers_per_chunk": [
    1,
    1,
    1,
    1,
    1,
    1,
    1,
    1,
    1,
    1
  ],
  "duplicates": 0,
  "id_sequence_equal": true,
  "field_values_equal": true,
  "source_sha256": "961a45aa41dbead9203a56b1e144c5b17623fc81b742fdae351c2fb85c6d01e3"
}

$ python split_csv.py split --source input/records.csv --output chunks10 --max-rows 10
exit_code: 0
{
  "rows": 10,
  "chunks": 1,
  "max_rows": 10,
  "rows_per_chunk": [
    10
  ],
  "headers_per_chunk": [
    1
  ],
  "duplicates": 0,
  "id_sequence_equal": true,
  "field_values_equal": true,
  "source_sha256": "961a45aa41dbead9203a56b1e144c5b17623fc81b742fdae351c2fb85c6d01e3"
}

$ python split_csv.py split --source input/eight_records.csv --output chunks8 --max-rows 4
exit_code: 0
{
  "rows": 8,
  "chunks": 2,
  "max_rows": 4,
  "rows_per_chunk": [
    4,
    4
  ],
  "headers_per_chunk": [
    1,
    1
  ],
  "duplicates": 0,
  "id_sequence_equal": true,
  "field_values_equal": true,
  "source_sha256": "a1415c1d3cfec8ae19777d31d6dbf04218e96d2cfd6c7fa65787abf7ef411914"
}

$ python split_csv.py split --source input/header_only.csv --output chunks0 --max-rows 4
exit_code: 0
{
  "rows": 0,
  "chunks": 0,
  "max_rows": 4,
  "rows_per_chunk": [],
  "headers_per_chunk": [],
  "duplicates": 0,
  "id_sequence_equal": true,
  "field_values_equal": true,
  "source_sha256": "197c0cf35f40f23432b6c3cc31c1d17a4299e8f6fb9d0e70bed6b16da1ecfa82"
}

$ python split_csv.py split --source input/failures/late_unclosed_quote.csv --output bad_chunks --max-rows 4
exit_code: 2
ERROR: unexpected end of data

$ python split_csv.py split --source input/records.csv --output chunks4 --max-rows 4
exit_code: 2
ERROR: Output already exists; choose a new name: chunks4

$ python split_csv.py split --source input/records.csv --output chunks4_again --max-rows 4
exit_code: 0
{
  "rows": 10,
  "chunks": 3,
  "max_rows": 4,
  "rows_per_chunk": [
    4,
    4,
    2
  ],
  "headers_per_chunk": [
    1,
    1,
    1
  ],
  "duplicates": 0,
  "id_sequence_equal": true,
  "field_values_equal": true,
  "source_sha256": "961a45aa41dbead9203a56b1e144c5b17623fc81b742fdae351c2fb85c6d01e3"
}

PASS: all article split/verify/boundary/failure/rerun commands; expected CSV bytes equal actual; all input hashes unchanged
