-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,11 @@ | ||||||||||||||||||||||||||||||||||
| defmodule Testcontainers.Util.PropertiesParser do | ||||||||||||||||||||||||||||||||||
| @moduledoc false | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @file_path "~/.testcontainers.properties" | ||||||||||||||||||||||||||||||||||
| @user_file "~/.testcontainers.properties" | ||||||||||||||||||||||||||||||||||
| @project_file ".testcontainers.properties" | ||||||||||||||||||||||||||||||||||
| @env_prefix "TESTCONTAINERS_" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| def read_property_file(file_path \\ @file_path) do | ||||||||||||||||||||||||||||||||||
| def read_property_file(file_path \\ @user_file) do | ||||||||||||||||||||||||||||||||||
| if File.exists?(Path.expand(file_path)) do | ||||||||||||||||||||||||||||||||||
| with {:ok, content} <- File.read(Path.expand(file_path)), | ||||||||||||||||||||||||||||||||||
| properties <- parse_properties(content) do | ||||||||||||||||||||||||||||||||||
|
|
@@ -18,6 +20,76 @@ defmodule Testcontainers.Util.PropertiesParser do | |||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| @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. | ||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||
| def read_property_sources(opts \\ []) do | ||||||||||||||||||||||||||||||||||
| user_file = Keyword.get(opts, :user_file, @user_file) | ||||||||||||||||||||||||||||||||||
| project_file = Keyword.get(opts, :project_file, @project_file) | ||||||||||||||||||||||||||||||||||
| env_prefix = Keyword.get(opts, :env_prefix, @env_prefix) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| project_props = read_file_silent(project_file) | ||||||||||||||||||||||||||||||||||
| user_props = read_file_silent(user_file) | ||||||||||||||||||||||||||||||||||
| env_props = read_env_vars(env_prefix) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # Merge in order of lowest to highest precedence | ||||||||||||||||||||||||||||||||||
| merged = | ||||||||||||||||||||||||||||||||||
| project_props | ||||||||||||||||||||||||||||||||||
| |> Map.merge(user_props) | ||||||||||||||||||||||||||||||||||
| |> Map.merge(env_props) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| {:ok, merged} | ||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| defp read_file_silent(file_path) do | ||||||||||||||||||||||||||||||||||
| expanded = Path.expand(file_path) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if File.exists?(expanded) do | ||||||||||||||||||||||||||||||||||
| case File.read(expanded) do | ||||||||||||||||||||||||||||||||||
| {:ok, content} -> parse_properties(content) | ||||||||||||||||||||||||||||||||||
| {:error, _} -> %{} | ||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||
| %{} | ||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| defp read_env_vars(prefix) do | ||||||||||||||||||||||||||||||||||
| System.get_env() | ||||||||||||||||||||||||||||||||||
| |> Enum.filter(fn {key, _value} -> String.starts_with?(key, prefix) end) | ||||||||||||||||||||||||||||||||||
| |> Enum.map(&env_to_property(&1, prefix)) | ||||||||||||||||||||||||||||||||||
| |> Map.new() | ||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # Converts TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED to ryuk.container.privileged | ||||||||||||||||||||||||||||||||||
| defp env_to_property({key, value}, prefix) do | ||||||||||||||||||||||||||||||||||
| property_key = | ||||||||||||||||||||||||||||||||||
| key | ||||||||||||||||||||||||||||||||||
| |> String.replace_prefix(prefix, "") | ||||||||||||||||||||||||||||||||||
| |> String.downcase() | ||||||||||||||||||||||||||||||||||
| |> String.replace("_", ".") | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+83
to
+88
|
||||||||||||||||||||||||||||||||||
| 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(".") |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,70 @@ | ||||||
| defmodule Testcontainers.Util.PropertiesParserTest do | ||||||
| use ExUnit.Case, async: false | ||||||
|
|
||||||
| alias Testcontainers.Util.PropertiesParser | ||||||
|
|
||||||
| describe "read_property_sources/0" do | ||||||
|
||||||
| describe "read_property_sources/0" do | |
| describe "read_property_sources/1" 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.
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.
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.
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).
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 |
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.