This is an automated email from the ASF dual-hosted git repository.

Gerrrr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/otava.git


The following commit(s) were added to refs/heads/master by this push:
     new a1ef00d  Make CSV importer configurable via ConfigArgParse (#166)
a1ef00d is described below

commit a1ef00de28f94c60c9ce9119f1d41b7ac6f6e9f1
Author: DanaerLee <[email protected]>
AuthorDate: Tue Aug 25 14:14:38 2026 +0800

    Make CSV importer configurable via ConfigArgParse (#166)
    
    CSV delimiter and quote character could only be set per test in YAML.
    They can now also be set through a top-level csv section in the config
    file, the --csv-delimiter and --csv-quote-char options, and the
    CSV_DELIMITER and CSV_QUOTE_CHAR environment variables. Explicit global
    values override per-test csv_options; otherwise existing behavior is
    unchanged.
    
    The per-test YAML key is renamed from quotechar to quote_char. The
    loader only ever read quote_char, so the documented quotechar spelling
    was silently ignored and no working configuration depends on it.
    
    Closes #119
---
 docs/CSV.md                                   | 18 ++++++-
 examples/csv/config/otava.yaml                |  2 +-
 otava/config.py                               | 16 ++++--
 otava/csv_options.py                          | 42 +++++++++++++++
 otava/test_config.py                          | 22 +++++---
 tests/cli_help_test.py                        | 77 ++++++++++++++++++++++++---
 tests/cli_options_test.py                     |  2 +-
 tests/config_test.py                          | 70 +++++++++++++++++++++++-
 tests/csv_e2e_test.py                         |  2 +-
 tests/resources/sample_config.yaml            |  3 ++
 tests/resources/substitution_test_config.yaml |  4 ++
 11 files changed, 236 insertions(+), 22 deletions(-)

diff --git a/docs/CSV.md b/docs/CSV.md
index 6d674bf..eef8013 100644
--- a/docs/CSV.md
+++ b/docs/CSV.md
@@ -34,9 +34,25 @@ tests:
     metrics: [metric1, metric2]
     csv_options:
       delimiter: ','
-      quotechar: "'"
+      quote_char: "'"
 ```
 
+## CSV options
+
+The delimiter and quote character can also be configured globally:
+
+```yaml
+csv:
+  delimiter: ';'
+  quote_char: "'"
+```
+
+The corresponding command-line options are `--csv-delimiter` and
+`--csv-quote-char`. They can also be set through the `CSV_DELIMITER` and
+`CSV_QUOTE_CHAR` environment variables. Explicit global values override the
+`csv_options` values of individual tests; without a global value, existing
+per-test settings and defaults are unchanged.
+
 ## Example
 
 ```bash
diff --git a/examples/csv/config/otava.yaml b/examples/csv/config/otava.yaml
index d91f4ea..7989aa1 100644
--- a/examples/csv/config/otava.yaml
+++ b/examples/csv/config/otava.yaml
@@ -24,4 +24,4 @@ tests:
     metrics: [metric1, metric2]
     csv_options:
       delimiter: ","
-      quotechar: "'"
+      quote_char: "'"
diff --git a/otava/config.py b/otava/config.py
index ae48ad4..7fecea5 100644
--- a/otava/config.py
+++ b/otava/config.py
@@ -23,6 +23,7 @@ import configargparse
 from ruamel.yaml import YAML
 
 from otava.bigquery import BigQueryConfig
+from otava.csv_options import CsvConfig
 from otava.grafana import GrafanaConfig
 from otava.graphite import GraphiteConfig
 from otava.influxdb import InfluxDBConfig
@@ -34,6 +35,7 @@ from otava.util import merge_dict_list
 
 @dataclass
 class Config:
+    csv: CsvConfig
     graphite: Optional[GraphiteConfig]
     grafana: Optional[GrafanaConfig]
     tests: Dict[str, TestConfig]
@@ -56,7 +58,9 @@ def load_templates(config: Dict) -> Dict[str, Dict]:
     return templates
 
 
-def load_tests(config: Dict, templates: Dict) -> Dict[str, TestConfig]:
+def load_tests(
+    config: Dict, templates: Dict, csv_config: Optional[CsvConfig] = None
+) -> Dict[str, TestConfig]:
     tests = config.get("tests", {})
     if not isinstance(tests, Dict):
         raise ConfigError("Property `tests` is not a dictionary")
@@ -71,7 +75,7 @@ def load_tests(config: Dict, templates: Dict) -> Dict[str, 
TestConfig]:
         except KeyError as e:
             raise ConfigError(f"Template {e.args[0]} referenced in test 
{test_name} not found")
         test_config = merge_dict_list(template_list + [test_config])
-        result[test_name] = create_test_config(test_name, test_config)
+        result[test_name] = create_test_config(test_name, test_config, 
csv_config)
 
     return result
 
@@ -98,13 +102,14 @@ def load_test_groups(config: Dict, tests: Dict[str, 
TestConfig]) -> Dict[str, Li
 
 
 def load_config_from_parser_args(args: configargparse.Namespace) -> Config:
+    csv_config = CsvConfig.from_parser_args(args)
     config_file = getattr(args, "config_file", None)
     if config_file is not None:
         yaml = YAML(typ="safe")
         config = yaml.load(Path(config_file).read_text())
 
         templates = load_templates(config)
-        tests = load_tests(config, templates)
+        tests = load_tests(config, templates, csv_config)
         groups = load_test_groups(config, tests)
     else:
         logging.warning("Otava configuration file not found or not specified")
@@ -112,6 +117,7 @@ def load_config_from_parser_args(args: 
configargparse.Namespace) -> Config:
         groups = {}
 
     return Config(
+        csv=csv_config,
         graphite=GraphiteConfig.from_parser_args(args),
         grafana=GrafanaConfig.from_parser_args(args),
         slack=SlackConfig.from_parser_args(args),
@@ -131,6 +137,7 @@ class 
NestedYAMLConfigFileParser(configargparse.ConfigFileParser):
     """
 
     CLI_CONFIG_SECTIONS = [
+        CsvConfig.NAME,
         GraphiteConfig.NAME,
         GrafanaConfig.NAME,
         SlackConfig.NAME,
@@ -178,7 +185,8 @@ class 
NestedYAMLConfigFileParser(configargparse.ConfigFileParser):
 
 
 def add_service_option_groups(parser) -> None:
-    """Add Graphite, Grafana, Slack, Postgres, and BigQuery option groups to a 
parser."""
+    """Add importer and integration option groups to a parser."""
+    CsvConfig.add_parser_args(parser.add_argument_group('CSV Options', 
'Options for CSV configuration'))
     GraphiteConfig.add_parser_args(parser.add_argument_group('Graphite 
Options', 'Options for Graphite configuration'))
     GrafanaConfig.add_parser_args(parser.add_argument_group('Grafana Options', 
'Options for Grafana configuration'))
     SlackConfig.add_parser_args(parser.add_argument_group('Slack Options', 
'Options for Slack configuration'))
diff --git a/otava/csv_options.py b/otava/csv_options.py
index 2c9ddb2..7ba82c5 100644
--- a/otava/csv_options.py
+++ b/otava/csv_options.py
@@ -17,6 +17,48 @@
 
 import enum
 from dataclasses import dataclass
+from typing import Optional
+
+import configargparse
+
+
+def single_character(value: str) -> str:
+    """The csv module only accepts single-character delimiters and quote 
characters."""
+    if len(value) != 1:
+        raise configargparse.ArgumentTypeError(f"must be a single character, 
got {value!r}")
+    return value
+
+
+@dataclass
+class CsvConfig:
+    NAME = "csv"
+
+    delimiter: Optional[str] = None
+    quote_char: Optional[str] = None
+
+    @staticmethod
+    def add_parser_args(arg_group):
+        arg_group.add_argument(
+            "--csv-delimiter",
+            help="CSV delimiter",
+            env_var="CSV_DELIMITER",
+            type=single_character,
+            default=configargparse.SUPPRESS,
+        )
+        arg_group.add_argument(
+            "--csv-quote-char",
+            help="CSV quote character",
+            env_var="CSV_QUOTE_CHAR",
+            type=single_character,
+            default=configargparse.SUPPRESS,
+        )
+
+    @staticmethod
+    def from_parser_args(args):
+        return CsvConfig(
+            delimiter=getattr(args, "csv_delimiter", None),
+            quote_char=getattr(args, "csv_quote_char", None),
+        )
 
 
 @dataclass
diff --git a/otava/test_config.py b/otava/test_config.py
index db24d42..75c2025 100644
--- a/otava/test_config.py
+++ b/otava/test_config.py
@@ -19,7 +19,7 @@ import os.path
 from dataclasses import dataclass
 from typing import Dict, List, Optional
 
-from otava.csv_options import CsvOptions
+from otava.csv_options import CsvConfig, CsvOptions
 
 
 @dataclass
@@ -235,7 +235,9 @@ class InfluxDBTestConfig(TestConfig):
         return list(self.metrics.keys())
 
 
-def create_test_config(name: str, config: Dict) -> TestConfig:
+def create_test_config(
+    name: str, config: Dict, csv_config: Optional[CsvConfig] = None
+) -> TestConfig:
     """
     Loads properties of a test from a dictionary read from otava's config file
     This dictionary must have the `type` property to determine the type of the 
test.
@@ -244,7 +246,7 @@ def create_test_config(name: str, config: Dict) -> 
TestConfig:
     """
     test_type = config.get("type")
     if test_type == "csv":
-        return create_csv_test_config(name, config)
+        return create_csv_test_config(name, config, csv_config)
     elif test_type == "graphite":
         return create_graphite_test_config(name, config)
     elif test_type == "histostat":
@@ -263,7 +265,9 @@ def create_test_config(name: str, config: Dict) -> 
TestConfig:
         raise TestConfigError(f"Unknown test type {test_type} for test {name}")
 
 
-def create_csv_test_config(test_name: str, test_info: Dict) -> CsvTestConfig:
+def create_csv_test_config(
+    test_name: str, test_info: Dict, csv_config: Optional[CsvConfig] = None
+) -> CsvTestConfig:
     csv_options = CsvOptions()
     try:
         file = test_info["file"]
@@ -293,8 +297,14 @@ def create_csv_test_config(test_name: str, test_info: 
Dict) -> CsvTestConfig:
         raise TestConfigError(f"Attributes of the test {test_name} must be a 
list")
 
     if test_info.get("csv_options"):
-        csv_options.delimiter = test_info["csv_options"].get("delimiter", ",")
-        csv_options.quote_char = test_info["csv_options"].get("quote_char", 
'"')
+        per_test_options = test_info["csv_options"]
+        csv_options.delimiter = per_test_options.get("delimiter", ",")
+        csv_options.quote_char = per_test_options.get("quote_char", '"')
+    if csv_config is not None:
+        if csv_config.delimiter is not None:
+            csv_options.delimiter = csv_config.delimiter
+        if csv_config.quote_char is not None:
+            csv_options.quote_char = csv_config.quote_char
     return CsvTestConfig(
         test_name,
         file,
diff --git a/tests/cli_help_test.py b/tests/cli_help_test.py
index f2ce443..06cb974 100644
--- a/tests/cli_help_test.py
+++ b/tests/cli_help_test.py
@@ -49,7 +49,8 @@ def test_otava_help_output():
     assert (
         result.stdout
         == """\
-usage: otava [-h] [--config-file CONFIG_FILE] [--graphite-url GRAPHITE_URL]
+usage: otava [-h] [--config-file CONFIG_FILE] [--csv-delimiter CSV_DELIMITER]
+             [--csv-quote-char CSV_QUOTE_CHAR] [--graphite-url GRAPHITE_URL]
              [--grafana-url GRAFANA_URL] [--grafana-user GRAFANA_USER]
              [--grafana-password GRAFANA_PASSWORD] [--slack-token SLACK_TOKEN]
              [--postgres-hostname POSTGRES_HOSTNAME] [--postgres-port 
POSTGRES_PORT]
@@ -75,6 +76,14 @@ options:
   --config-file CONFIG_FILE
                         Otava config file path [env var: OTAVA_CONFIG]
 
+CSV Options:
+  Options for CSV configuration
+
+  --csv-delimiter CSV_DELIMITER
+                        CSV delimiter [env var: CSV_DELIMITER]
+  --csv-quote-char CSV_QUOTE_CHAR
+                        CSV quote character [env var: CSV_QUOTE_CHAR]
+
 Graphite Options:
   Options for Graphite configuration
 
@@ -159,7 +168,8 @@ def test_otava_analyze_help_output():
         magnitude_option = "  -M MAGNITUDE, --magnitude MAGNITUDE"
 
     usage_and_options = f"""\
-usage: otava analyze [-h] [--config-file CONFIG_FILE] [--graphite-url 
GRAPHITE_URL]
+usage: otava analyze [-h] [--config-file CONFIG_FILE] [--csv-delimiter 
CSV_DELIMITER]
+                     [--csv-quote-char CSV_QUOTE_CHAR] [--graphite-url 
GRAPHITE_URL]
                      [--grafana-url GRAFANA_URL] [--grafana-user GRAFANA_USER]
                      [--grafana-password GRAFANA_PASSWORD] [--slack-token 
SLACK_TOKEN]
                      [--postgres-hostname POSTGRES_HOSTNAME] [--postgres-port 
POSTGRES_PORT]
@@ -228,6 +238,14 @@ options:
   --orig-edivisive      use the original edivisive algorithm with no windowing 
and weak change
                         points analysis improvements
 
+CSV Options:
+  Options for CSV configuration
+
+  --csv-delimiter CSV_DELIMITER
+                        CSV delimiter [env var: CSV_DELIMITER]
+  --csv-quote-char CSV_QUOTE_CHAR
+                        CSV quote character [env var: CSV_QUOTE_CHAR]
+
 Graphite Options:
   Options for Graphite configuration
 
@@ -301,7 +319,8 @@ def test_otava_list_tests_help_output():
     assert (
         result.stdout
         == """\
-usage: otava list-tests [-h] [--config-file CONFIG_FILE] [--graphite-url 
GRAPHITE_URL]
+usage: otava list-tests [-h] [--config-file CONFIG_FILE] [--csv-delimiter 
CSV_DELIMITER]
+                        [--csv-quote-char CSV_QUOTE_CHAR] [--graphite-url 
GRAPHITE_URL]
                         [--grafana-url GRAFANA_URL] [--grafana-user 
GRAFANA_USER]
                         [--grafana-password GRAFANA_PASSWORD] [--slack-token 
SLACK_TOKEN]
                         [--postgres-hostname POSTGRES_HOSTNAME] 
[--postgres-port POSTGRES_PORT]
@@ -323,6 +342,14 @@ options:
   --config-file CONFIG_FILE
                         Otava config file path [env var: OTAVA_CONFIG]
 
+CSV Options:
+  Options for CSV configuration
+
+  --csv-delimiter CSV_DELIMITER
+                        CSV delimiter [env var: CSV_DELIMITER]
+  --csv-quote-char CSV_QUOTE_CHAR
+                        CSV quote character [env var: CSV_QUOTE_CHAR]
+
 Graphite Options:
   Options for Graphite configuration
 
@@ -393,7 +420,8 @@ def test_otava_list_metrics_help_output():
     assert (
         result.stdout
         == """\
-usage: otava list-metrics [-h] [--config-file CONFIG_FILE] [--graphite-url 
GRAPHITE_URL]
+usage: otava list-metrics [-h] [--config-file CONFIG_FILE] [--csv-delimiter 
CSV_DELIMITER]
+                          [--csv-quote-char CSV_QUOTE_CHAR] [--graphite-url 
GRAPHITE_URL]
                           [--grafana-url GRAFANA_URL] [--grafana-user 
GRAFANA_USER]
                           [--grafana-password GRAFANA_PASSWORD] [--slack-token 
SLACK_TOKEN]
                           [--postgres-hostname POSTGRES_HOSTNAME] 
[--postgres-port POSTGRES_PORT]
@@ -415,6 +443,14 @@ options:
   --config-file CONFIG_FILE
                         Otava config file path [env var: OTAVA_CONFIG]
 
+CSV Options:
+  Options for CSV configuration
+
+  --csv-delimiter CSV_DELIMITER
+                        CSV delimiter [env var: CSV_DELIMITER]
+  --csv-quote-char CSV_QUOTE_CHAR
+                        CSV quote character [env var: CSV_QUOTE_CHAR]
+
 Graphite Options:
   Options for Graphite configuration
 
@@ -486,7 +522,8 @@ def test_otava_list_groups_help_output():
     assert (
         result.stdout
         == """\
-usage: otava list-groups [-h] [--config-file CONFIG_FILE] [--graphite-url 
GRAPHITE_URL]
+usage: otava list-groups [-h] [--config-file CONFIG_FILE] [--csv-delimiter 
CSV_DELIMITER]
+                         [--csv-quote-char CSV_QUOTE_CHAR] [--graphite-url 
GRAPHITE_URL]
                          [--grafana-url GRAFANA_URL] [--grafana-user 
GRAFANA_USER]
                          [--grafana-password GRAFANA_PASSWORD] [--slack-token 
SLACK_TOKEN]
                          [--postgres-hostname POSTGRES_HOSTNAME] 
[--postgres-port POSTGRES_PORT]
@@ -504,6 +541,14 @@ options:
   --config-file CONFIG_FILE
                         Otava config file path [env var: OTAVA_CONFIG]
 
+CSV Options:
+  Options for CSV configuration
+
+  --csv-delimiter CSV_DELIMITER
+                        CSV delimiter [env var: CSV_DELIMITER]
+  --csv-quote-char CSV_QUOTE_CHAR
+                        CSV quote character [env var: CSV_QUOTE_CHAR]
+
 Graphite Options:
   Options for Graphite configuration
 
@@ -574,7 +619,8 @@ def test_otava_remove_annotations_help_output():
     assert (
         result.stdout
         == """\
-usage: otava remove-annotations [-h] [--config-file CONFIG_FILE] 
[--graphite-url GRAPHITE_URL]
+usage: otava remove-annotations [-h] [--config-file CONFIG_FILE] 
[--csv-delimiter CSV_DELIMITER]
+                                [--csv-quote-char CSV_QUOTE_CHAR] 
[--graphite-url GRAPHITE_URL]
                                 [--grafana-url GRAFANA_URL] [--grafana-user 
GRAFANA_USER]
                                 [--grafana-password GRAFANA_PASSWORD] 
[--slack-token SLACK_TOKEN]
                                 [--postgres-hostname POSTGRES_HOSTNAME]
@@ -599,6 +645,14 @@ options:
                         Otava config file path [env var: OTAVA_CONFIG]
   --force               don't ask questions, just do it
 
+CSV Options:
+  Options for CSV configuration
+
+  --csv-delimiter CSV_DELIMITER
+                        CSV delimiter [env var: CSV_DELIMITER]
+  --csv-quote-char CSV_QUOTE_CHAR
+                        CSV quote character [env var: CSV_QUOTE_CHAR]
+
 Graphite Options:
   Options for Graphite configuration
 
@@ -669,7 +723,8 @@ def test_otava_validate_help_output():
     assert (
         result.stdout
         == """\
-usage: otava validate [-h] [--config-file CONFIG_FILE] [--graphite-url 
GRAPHITE_URL]
+usage: otava validate [-h] [--config-file CONFIG_FILE] [--csv-delimiter 
CSV_DELIMITER]
+                      [--csv-quote-char CSV_QUOTE_CHAR] [--graphite-url 
GRAPHITE_URL]
                       [--grafana-url GRAFANA_URL] [--grafana-user GRAFANA_USER]
                       [--grafana-password GRAFANA_PASSWORD] [--slack-token 
SLACK_TOKEN]
                       [--postgres-hostname POSTGRES_HOSTNAME] [--postgres-port 
POSTGRES_PORT]
@@ -687,6 +742,14 @@ options:
   --config-file CONFIG_FILE
                         Otava config file path [env var: OTAVA_CONFIG]
 
+CSV Options:
+  Options for CSV configuration
+
+  --csv-delimiter CSV_DELIMITER
+                        CSV delimiter [env var: CSV_DELIMITER]
+  --csv-quote-char CSV_QUOTE_CHAR
+                        CSV quote character [env var: CSV_QUOTE_CHAR]
+
 Graphite Options:
   Options for Graphite configuration
 
diff --git a/tests/cli_options_test.py b/tests/cli_options_test.py
index d578f14..d5f4aeb 100644
--- a/tests/cli_options_test.py
+++ b/tests/cli_options_test.py
@@ -115,7 +115,7 @@ def _create_csv_config_file_for_test(td_path: Path):
                 metrics: [metric1, metric2]
                 csv_options:
                     delimiter: ","
-                    quotechar: "'"
+                    quote_char: "'"
         """
     )
     config_path = td_path / "otava.yaml"
diff --git a/tests/config_test.py b/tests/config_test.py
index 58b223f..d0a031d 100644
--- a/tests/config_test.py
+++ b/tests/config_test.py
@@ -25,7 +25,12 @@ from otava.config import (
     load_config_from_file,
 )
 from otava.main import create_otava_cli_parser
-from otava.test_config import CsvTestConfig, GraphiteTestConfig, 
HistoStatTestConfig
+from otava.test_config import (
+    CsvTestConfig,
+    GraphiteTestConfig,
+    HistoStatTestConfig,
+    create_csv_test_config,
+)
 
 
 def test_load_graphite_tests():
@@ -54,6 +59,8 @@ def test_load_csv_tests():
     assert len(test.metrics) == 2
     assert len(test.attributes) == 1
     assert test.file == "tests/resources/sample.csv"
+    assert test.csv_options.delimiter == ","
+    assert test.csv_options.quote_char == "'"
 
     test = tests["local2"]
     assert isinstance(test, CsvTestConfig)
@@ -66,6 +73,40 @@ def test_load_csv_tests():
     assert test.file == "tests/resources/sample.csv"
 
 
+def test_per_test_csv_quote_character_config_name():
+    test = create_csv_test_config(
+        "local",
+        {"file": "sample.csv", "metrics": [], "csv_options": {"quote_char": 
"|"}},
+    )
+
+    assert test.csv_options.quote_char == "|"
+
+
+def test_global_csv_quote_character_config_name():
+    parser = NestedYAMLConfigFileParser()
+    result = parser.parse(StringIO("csv:\n  quote_char: '|'\n"))
+
+    assert result == {"csv-quote-char": "|"}
+
+
[email protected]("option", ["--csv-delimiter", "--csv-quote-char"])
+def test_csv_options_reject_multiple_characters(option, tmp_path):
+    config_file = tmp_path / "otava.yaml"
+    config_file.write_text("tests: {}\n")
+
+    with pytest.raises(SystemExit):
+        load_config_from_file(str(config_file), arg_overrides=[option, "::"])
+
+
[email protected]("key", ["delimiter", "quote_char"])
+def test_csv_config_file_options_reject_multiple_characters(key, tmp_path):
+    config_file = tmp_path / "otava.yaml"
+    config_file.write_text(f'csv:\n  {key}: "::"\ntests: {{}}\n')
+
+    with pytest.raises(SystemExit):
+        load_config_from_file(str(config_file))
+
+
 def test_load_test_groups():
     config = load_config_from_file("tests/resources/sample_config.yaml")
     groups = config.test_groups
@@ -100,6 +141,8 @@ def test_load_histostat_config():
         ("postgres_username", lambda c: c.postgres.username, 
"POSTGRES_USERNAME", "--postgres-username"),
         ("postgres_password", lambda c: c.postgres.password, 
"POSTGRES_PASSWORD", "--postgres-password"),
         ("postgres_database", lambda c: c.postgres.database, 
"POSTGRES_DATABASE", "--postgres-database"),
+        ("csv_delimiter", lambda c: c.csv.delimiter, "CSV_DELIMITER", 
"--csv-delimiter", ";", "|", "\t"),
+        ("csv_quote_char", lambda c: c.csv.quote_char, "CSV_QUOTE_CHAR", 
"--csv-quote-char", "'", "|", "`"),
     ],
     ids=lambda v: v[0],  # use the property name for the parameterized test 
name
 )
@@ -216,6 +259,31 @@ templates:
         assert section not in ignored_sections, f"Found key '{key}' from 
ignored section '{section}'"
 
 
+def test_csv_configargparse_options_apply_to_csv_tests():
+    config = load_config_from_file(
+        "tests/resources/sample_config.yaml",
+        arg_overrides=["--csv-delimiter", ";", "--csv-quote-char", "'"],
+    )
+
+    assert config.tests["local1"].csv_options.delimiter == ";"
+    assert config.tests["local1"].csv_options.quote_char == "'"
+    assert config.tests["local2"].csv_options.delimiter == ";"
+    assert config.tests["local2"].csv_options.quote_char == "'"
+
+
[email protected](
+    "args",
+    [
+        ["--csv-delimiter", ";", "analyze", "local1"],
+        ["analyze", "local1", "--csv-delimiter", ";"],
+    ],
+)
+def test_csv_cli_options_can_appear_before_or_after_subcommand(args):
+    parsed = create_otava_cli_parser().parse_args(args)
+
+    assert parsed.csv_delimiter == ";"
+
+
 def test_cli_precedence_over_env_vars():
     """Test that CLI arguments take precedence over environment variables."""
 
diff --git a/tests/csv_e2e_test.py b/tests/csv_e2e_test.py
index 6afb422..25c90a3 100644
--- a/tests/csv_e2e_test.py
+++ b/tests/csv_e2e_test.py
@@ -63,7 +63,7 @@ def test_analyze_csv():
             metrics: [metric1, metric2]
             csv_options:
               delimiter: ","
-              quotechar: "'"
+              quote_char: "'"
         """
     )
     expected_output = textwrap.dedent(
diff --git a/tests/resources/sample_config.yaml 
b/tests/resources/sample_config.yaml
index 995204a..ca442e0 100644
--- a/tests/resources/sample_config.yaml
+++ b/tests/resources/sample_config.yaml
@@ -89,6 +89,9 @@ tests:
     time_column: time
     metrics: [metric1, metric2]
     attributes: [commit]
+    csv_options:
+      delimiter: ","
+      quote_char: "'"
 
   local2:
     type: csv
diff --git a/tests/resources/substitution_test_config.yaml 
b/tests/resources/substitution_test_config.yaml
index 6b6b024..a1d767a 100644
--- a/tests/resources/substitution_test_config.yaml
+++ b/tests/resources/substitution_test_config.yaml
@@ -37,3 +37,7 @@ postgres:
   username: config_postgres_username
   password: config_postgres_password
   database: config_postgres_database
+
+csv:
+  delimiter: ";"
+  quote_char: "'"

Reply via email to