-
Notifications
You must be signed in to change notification settings - Fork 27
Support Multiple Property Sources With Precedence #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
1. Environment variables (TESTCONTAINERS_* prefix) 2. User file (~/.testcontainers.properties) 3. Project file (.testcontainers.properties)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for reading Testcontainers configuration from multiple property sources with proper precedence ordering, aligning with Java and Go implementations. The implementation introduces read_property_sources/1 which merges configuration from environment variables, user files, and project files, with environment variables having the highest priority.
Key Changes:
- Introduces
read_property_sources/1that reads from three sources: env vars (TESTCONTAINERS_*), user file (~/.testcontainers.properties), and project file (.testcontainers.properties) - Environment variables are converted from
TESTCONTAINERS_PROPERTY_NAMEformat toproperty.nameformat - Maintains backward compatibility with existing
read_property_file/1function
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| lib/util/properties.ex | Adds new read_property_sources/1 function with helper methods for reading multiple property sources and environment variable conversion |
| test/util/properties_test.exs | Adds test coverage for the new multi-source reading functionality and environment variable precedence |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| alias Testcontainers.Util.PropertiesParser | ||
|
|
||
| describe "read_property_sources/0" do |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The describe block header says "read_property_sources/0" but the function signature in the implementation is "read_property_sources(opts \ [])". This is technically read_property_sources/1, not /0. Update the describe block to use "read_property_sources/1" for consistency with Elixir naming conventions.
| describe "read_property_sources/0" do | |
| describe "read_property_sources/1" do |
| describe "read_property_sources/0" do | ||
| test "returns empty map when no files or env vars exist" do | ||
| # Clean env vars that might interfere | ||
| System.get_env() | ||
| |> Enum.filter(fn {k, _} -> String.starts_with?(k, "TESTCONTAINERS_") end) | ||
| |> Enum.each(fn {k, _} -> System.delete_env(k) end) | ||
|
|
||
| {:ok, props} = PropertiesParser.read_property_sources() | ||
|
|
||
| # Should at least return a map (may have project file props) | ||
| assert is_map(props) | ||
| end | ||
|
|
||
| test "reads environment variables with TESTCONTAINERS_ prefix" do | ||
| System.put_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") | ||
| System.put_env("TESTCONTAINERS_SOME_OTHER_PROPERTY", "value") | ||
|
|
||
| {:ok, props} = PropertiesParser.read_property_sources() | ||
|
|
||
| assert props["ryuk.container.privileged"] == "true" | ||
| assert props["some.other.property"] == "value" | ||
|
|
||
| # Cleanup | ||
| System.delete_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED") | ||
| System.delete_env("TESTCONTAINERS_SOME_OTHER_PROPERTY") | ||
| end | ||
|
|
||
| test "environment variables take precedence over file properties" do | ||
| # Set env var | ||
| System.put_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "from_env") | ||
|
|
||
| {:ok, props} = PropertiesParser.read_property_sources() | ||
|
|
||
| # Env should win over any file-based setting | ||
| assert props["ryuk.container.privileged"] == "from_env" | ||
|
|
||
| # Cleanup | ||
| System.delete_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED") | ||
| end | ||
| end |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for the precedence order between user file and project file. The current tests only verify env vars vs files, but don't test that user file takes precedence over project file when both exist. Consider adding a test that uses custom file paths (via opts) to verify the complete precedence chain: env vars > user file > project file.
| describe "read_property_sources/0" do | ||
| test "returns empty map when no files or env vars exist" do | ||
| # Clean env vars that might interfere | ||
| System.get_env() | ||
| |> Enum.filter(fn {k, _} -> String.starts_with?(k, "TESTCONTAINERS_") end) | ||
| |> Enum.each(fn {k, _} -> System.delete_env(k) end) | ||
|
|
||
| {:ok, props} = PropertiesParser.read_property_sources() | ||
|
|
||
| # Should at least return a map (may have project file props) | ||
| assert is_map(props) | ||
| end | ||
|
|
||
| test "reads environment variables with TESTCONTAINERS_ prefix" do | ||
| System.put_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") | ||
| System.put_env("TESTCONTAINERS_SOME_OTHER_PROPERTY", "value") | ||
|
|
||
| {:ok, props} = PropertiesParser.read_property_sources() | ||
|
|
||
| assert props["ryuk.container.privileged"] == "true" | ||
| assert props["some.other.property"] == "value" | ||
|
|
||
| # Cleanup | ||
| System.delete_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED") | ||
| System.delete_env("TESTCONTAINERS_SOME_OTHER_PROPERTY") | ||
| end | ||
|
|
||
| test "environment variables take precedence over file properties" do | ||
| # Set env var | ||
| System.put_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "from_env") | ||
|
|
||
| {:ok, props} = PropertiesParser.read_property_sources() | ||
|
|
||
| # Env should win over any file-based setting | ||
| assert props["ryuk.container.privileged"] == "from_env" | ||
|
|
||
| # Cleanup | ||
| System.delete_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED") | ||
| end | ||
| end |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for the custom options (:user_file, :project_file, :env_prefix) documented in the function. These options are part of the public API and should be tested to ensure they work correctly. Consider adding tests that verify custom file paths and custom env prefix work as expected.
| @doc """ | ||
| Reads properties from all sources with proper precedence. | ||
| Configuration is read from three sources with the following precedence | ||
| (highest to lowest): | ||
| 1. Environment variables (TESTCONTAINERS_* prefix) | ||
| 2. User file (~/.testcontainers.properties) | ||
| 3. Project file (.testcontainers.properties) | ||
| Environment variables are converted from TESTCONTAINERS_PROPERTY_NAME format | ||
| to property.name format (uppercase to lowercase, underscores to dots, prefix removed). | ||
| ## Options | ||
| - `:user_file` - path to user properties file (default: ~/.testcontainers.properties) | ||
| - `:project_file` - path to project properties file (default: .testcontainers.properties) | ||
| - `:env_prefix` - environment variable prefix (default: TESTCONTAINERS_) | ||
| Returns `{:ok, map}` with merged properties. | ||
| """ |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation says the function accepts opts with keyword options, but the title uses "read_property_sources/0" in the doc string. Since the function signature is read_property_sources(opts \\ []), the function documentation should be consistent and use the actual arity. Update the internal doc reference or clarify that it can be called as read_property_sources/0 OR read_property_sources/1.
| defp env_to_property({key, value}, prefix) do | ||
| property_key = | ||
| key | ||
| |> String.replace_prefix(prefix, "") | ||
| |> String.downcase() | ||
| |> String.replace("_", ".") |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The env_to_property function does not handle the case where a property name could have multiple consecutive underscores. For example, "TESTCONTAINERS_RYUK__PRIVILEGED" would be converted to "ryuk..privileged" (with double dots). While this may be an edge case, consider whether consecutive underscores should be treated differently or if the current behavior is acceptable for this use case.
| defp env_to_property({key, value}, prefix) do | |
| property_key = | |
| key | |
| |> String.replace_prefix(prefix, "") | |
| |> String.downcase() | |
| |> String.replace("_", ".") | |
| # Also handles consecutive underscores by treating them as a single separator, | |
| # e.g. TESTCONTAINERS_RYUK__PRIVILEGED -> ryuk.privileged | |
| defp env_to_property({key, value}, prefix) do | |
| property_key = | |
| key | |
| |> String.replace_prefix(prefix, "") | |
| |> String.downcase() | |
| |> String.split("_") | |
| |> Enum.reject(&(&1 == "")) | |
| |> Enum.join(".") |
| describe "read_property_sources/0" do | ||
| test "returns empty map when no files or env vars exist" do | ||
| # Clean env vars that might interfere | ||
| System.get_env() | ||
| |> Enum.filter(fn {k, _} -> String.starts_with?(k, "TESTCONTAINERS_") end) | ||
| |> Enum.each(fn {k, _} -> System.delete_env(k) end) | ||
|
|
||
| {:ok, props} = PropertiesParser.read_property_sources() | ||
|
|
||
| # Should at least return a map (may have project file props) | ||
| assert is_map(props) | ||
| end | ||
|
|
||
| test "reads environment variables with TESTCONTAINERS_ prefix" do | ||
| System.put_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "true") | ||
| System.put_env("TESTCONTAINERS_SOME_OTHER_PROPERTY", "value") | ||
|
|
||
| {:ok, props} = PropertiesParser.read_property_sources() | ||
|
|
||
| assert props["ryuk.container.privileged"] == "true" | ||
| assert props["some.other.property"] == "value" | ||
|
|
||
| # Cleanup | ||
| System.delete_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED") | ||
| System.delete_env("TESTCONTAINERS_SOME_OTHER_PROPERTY") | ||
| end | ||
|
|
||
| test "environment variables take precedence over file properties" do | ||
| # Set env var | ||
| System.put_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED", "from_env") | ||
|
|
||
| {:ok, props} = PropertiesParser.read_property_sources() | ||
|
|
||
| # Env should win over any file-based setting | ||
| assert props["ryuk.container.privileged"] == "from_env" | ||
|
|
||
| # Cleanup | ||
| System.delete_env("TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED") | ||
| end | ||
| end |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for error handling in read_file_silent when File.read returns an error. The function silently returns an empty map when File.read fails (line 68), but there's no test verifying this behavior. Consider adding a test that verifies the function gracefully handles file read errors (e.g., permission denied).
| end | ||
| end | ||
|
|
||
| describe "read_property_file/0" do |
Copilot
AI
Dec 23, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The describe block says "read_property_file/0" but tests the default behavior. Since read_property_file has a default argument, it's actually read_property_file/1 with an optional argument. Consider using "read_property_file/0 (default path)" or "read_property_file with defaults" to be clearer about what's being tested.
| describe "read_property_file/0" do | |
| describe "read_property_file/0 (default path)" do |
Adds
read_property_sources/1to read configuration from multiple sources with proper precedence: env vars > user file > project file. Matches the behavior of Java and Go implementations.TESTCONTAINERS_*environment variables (converted to dot notation)~/.testcontainers.propertiesand.testcontainers.propertiesread_property_file/1for backward compat