Skip to content

Commit 314dea2

Browse files
committed
fix: Treat a repeated override file path in different case as a repeat on Windows
The builder dropped repeated paths by exact string match, so on Windows a path listed twice in different case survived as two paths, and every key of that file failed as a duplicate of itself under the default fail handling. Paths are now compared with os.path.normcase, which ignores case on Windows and compares exactly elsewhere. The first spelling keeps its place in the order.
1 parent 82d1f0f commit 314dea2

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

‎ldclient/integrations/overrides.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"""
2828

2929
import math
30+
import os
3031
import threading
3132
from typing import List, Optional, Union
3233

@@ -158,9 +159,17 @@ def build(self, config: DataSourceBuilderConfig) -> OverrideSource: # pylint: d
158159
poll_interval = self.MINIMUM_POLL_INTERVAL
159160

160161
# A path listed more than once would make every entry of that file a duplicate of itself,
161-
# so the repeats are dropped and the first occurrence keeps its place in the order.
162+
# so the repeats are dropped and the first occurrence keeps its place in the order. Paths
163+
# are compared the way the platform compares file names: without regard to case on
164+
# Windows, exactly elsewhere.
162165
all_paths = abs_file_paths(self.__paths)
163-
paths = list(dict.fromkeys(all_paths))
166+
paths: List[str] = []
167+
seen = set()
168+
for path in all_paths:
169+
key = os.path.normcase(path)
170+
if key not in seen:
171+
seen.add(key)
172+
paths.append(path)
164173
if len(paths) != len(all_paths):
165174
log.warning("The file-based override source lists the same file more than once; the repeats are ignored")
166175

‎ldclient/testing/integrations/test_file_override_source.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
absent files, change detection in both modes, failure retention, and its Info log.
44
"""
55
import logging
6+
import ntpath
67
import os
78
import threading
89
import time
@@ -135,6 +136,24 @@ def test_builder_drops_a_repeated_path_and_warns(caplog):
135136
assert any('more than once' in r.getMessage() for r in caplog.records)
136137

137138

139+
def test_builder_treats_a_path_repeated_in_different_case_as_a_repeat_only_where_file_names_ignore_case():
140+
source = FileOverrideSourceBuilder(['/a.json', '/A.json']).build(SDKConfig('SDK_KEY'))
141+
assert isinstance(source, _FileOverrideSource)
142+
if os.name == 'nt':
143+
assert source._paths == [os.path.abspath('/a.json')]
144+
else:
145+
assert source._paths == [os.path.abspath('/a.json'), os.path.abspath('/A.json')]
146+
147+
148+
def test_builder_drops_a_path_repeated_in_different_case_on_windows(monkeypatch):
149+
# Windows file names ignore case, so two spellings of one path name the same file. The path
150+
# comparison of Windows is used here on any platform.
151+
monkeypatch.setattr(os.path, 'normcase', ntpath.normcase)
152+
source = FileOverrideSourceBuilder(['/a.json', '/b.json', '/A.json']).build(SDKConfig('SDK_KEY'))
153+
assert isinstance(source, _FileOverrideSource)
154+
assert source._paths == [os.path.abspath('/a.json'), os.path.abspath('/b.json')]
155+
156+
138157
def test_builder_polls_by_default(tmp_path):
139158
path = os.path.join(str(tmp_path), 'overrides.json')
140159
source = FileOverrideSourceBuilder([path]).build(SDKConfig('SDK_KEY'))

0 commit comments

Comments
 (0)