Merge lp:~a1s/brz/3.2 into lp:brz/3.2

Proposed by Aleksandr Smyshliaev
Status: Needs review
Proposed branch: lp:~a1s/brz/3.2
Merge into: lp:brz/3.2
Diff against target: 671 lines (+216/-99)
9 files modified
breezy/_walkdirs_win32.pyx (+19/-17)
breezy/osutils.py (+2/-1)
breezy/plugins/upload/tests/test_upload.py (+13/-0)
breezy/tests/__init__.py (+31/-11)
breezy/tests/test__walkdirs_win32.py (+13/-16)
breezy/tests/test_bedding.py (+28/-5)
breezy/tests/test_config.py (+42/-19)
breezy/tests/test_osutils.py (+63/-27)
breezy/transport/local.py (+5/-3)
To merge this branch: bzr merge lp:~a1s/brz/3.2
Reviewer Review Type Date Requested Status
Breezy developers Pending
Review via email: mp+412781@code.launchpad.net

Description of the change

This is an ongoing effort to make Breezy work on Windows

To post a comment you must log in.
Revision history for this message
Aleksandr Smyshliaev (a1s) wrote :

This includes changes from the merge proposal https://code.launchpad.net/~a1s/brz/3.2-windows-test_osutils/+merge/412025

The other thing hat has been fixed after that is passing the regression tests run by brz selftes test_config.

Revision history for this message
Jelmer Vernooij (jelmer) :
Revision history for this message
Aleksandr Smyshliaev (a1s) :
Revision history for this message
Jelmer Vernooij (jelmer) :
Revision history for this message
Aleksandr Smyshliaev (a1s) :
lp:~a1s/brz/3.2 updated
7564. By Aleksandr Smyshliaev

Merge brz/3.2

7565. By Aleksandr Smyshliaev

Get rid of osutils.open_file: in Python 3.4 and newer file descriptors are non-inheritable by default

7566. By Aleksandr Smyshliaev

Use relative import paths in test__walkdirs_win32

Revision history for this message
Aleksandr Smyshliaev (a1s) :
Revision history for this message
Jelmer Vernooij (jelmer) :
lp:~a1s/brz/3.2 updated
7567. By Aleksandr Smyshliaev

Fix issues discussed in https://code.launchpad.net/~a1s/brz/3.2/+merge/412781

7568. By Aleksandr Smyshliaev

Merge lp:brz/3.2

Revision history for this message
Aleksandr Smyshliaev (a1s) wrote :

Revision 7568 is not ready to be merged, sorry. It went OK on Windows, but then I found that it's broken on *nix. I'm going to fix it and make another commit in a day or two.

lp:~a1s/brz/3.2 updated
7569. By Aleksandr Smyshliaev

Add win32utils.list_fs_types()

7570. By Aleksandr Smyshliaev

Fix regression on *nix

Revision history for this message
Aleksandr Smyshliaev (a1s) wrote :

Ok, now this is tested on Linux/Python 3.10. Both test_config and test_osutils run without errors.

On Windows, test_config runs without errors, and test_osutils has two failures in test_osutils.Test_CICPCanonicalRelpath. Those I want to leave for another time.

I've replied to all questions asked here in diff comments.

Please check if this can be merged now.

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

It would still be useful to split this merge proposal into several separate ones, e.g. one with the filesystem type changes, one with the symlink changes, one with the config changes, one with the walkdir fixes, etc.

Revision history for this message
Aleksandr Smyshliaev (a1s) wrote :

> It would still be useful to split this merge proposal into several separate
> ones, e.g. one with the filesystem type changes, one with the symlink changes,
> one with the config changes, one with the walkdir fixes, etc.

I surely see the point in submitting each atomic change in a different merge proposal.

The problem is, I'm not sure the regression would run with just one of those things changed and others left as is. Perhaps I would try that if the merge proposals are reviewed in hours, not weeks.

At the moment, my first priority is getting qbrz work on Linux, and then I'm going to take a pause to handle my RL issues. I'd really like to have Breezy working on Windows, but right now Bazaar 2.6 covers my needs there.

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

> > It would still be useful to split this merge proposal into several separate
> > ones, e.g. one with the filesystem type changes, one with the symlink
> changes,
> > one with the config changes, one with the walkdir fixes, etc.
>
> I surely see the point in submitting each atomic change in a different merge
> proposal.
>
> The problem is, I'm not sure the regression would run with just one of those
> things changed and others left as is. Perhaps I would try that if the merge
> proposals are reviewed in hours, not weeks.
The delay in reviews is actually a function of the size of the merge proposals; for the smaller ones that are easy to understand the full implications of, I usually just process them as part of my regular email reading but for larger ones I need to schedule some time to sit down and understand them.

Jelmer

Revision history for this message
Aleksandr Smyshliaev (a1s) wrote :

I do understand. I'm sorry there are too many things to address at once.

lp:~a1s/brz/3.2 updated
7571. By Aleksandr Smyshliaev

Merge lp:brz/3.2

7572. By Aleksandr Smyshliaev

Merge lp:brz/3.2

Unmerged revisions

7572. By Aleksandr Smyshliaev

Merge lp:brz/3.2

7571. By Aleksandr Smyshliaev

Merge lp:brz/3.2

7570. By Aleksandr Smyshliaev

Fix regression on *nix

7569. By Aleksandr Smyshliaev

Add win32utils.list_fs_types()

7568. By Aleksandr Smyshliaev

Merge lp:brz/3.2

7567. By Aleksandr Smyshliaev

Fix issues discussed in https://code.launchpad.net/~a1s/brz/3.2/+merge/412781

7566. By Aleksandr Smyshliaev

Use relative import paths in test__walkdirs_win32

7565. By Aleksandr Smyshliaev

Get rid of osutils.open_file: in Python 3.4 and newer file descriptors are non-inheritable by default

7564. By Aleksandr Smyshliaev

Merge brz/3.2

7563. By Aleksandr Smyshliaev

Fix regression tests run by selftest test_config on Windows

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'breezy/_walkdirs_win32.pyx'
2--- breezy/_walkdirs_win32.pyx 2020-06-08 19:42:53 +0000
3+++ breezy/_walkdirs_win32.pyx 2022-01-18 07:08:36 +0000
4@@ -58,14 +58,11 @@
5
6 int GetLastError()
7
8- # Wide character functions
9- DWORD wcslen(WCHAR *)
10-
11
12 cdef extern from "Python.h":
13- WCHAR *PyUnicode_AS_UNICODE(object)
14- Py_ssize_t PyUnicode_GET_SIZE(object)
15- object PyUnicode_FromUnicode(WCHAR *, Py_ssize_t)
16+ WCHAR *PyUnicode_AsWideCharString(object, Py_ssize_t *size)
17+ Py_ssize_t PyUnicode_GET_LENGTH(object)
18+ object PyUnicode_FromWideChar(WCHAR *, Py_ssize_t)
19 int PyList_Append(object, object) except -1
20 object PyUnicode_AsUTF8String(object)
21
22@@ -120,12 +117,6 @@
23 self.st_mtime, self.st_ctime))
24
25
26-cdef object _get_name(WIN32_FIND_DATAW *data):
27- """Extract the Unicode name for this file/dir."""
28- return PyUnicode_FromUnicode(data.cFileName,
29- wcslen(data.cFileName))
30-
31-
32 cdef int _get_mode_bits(WIN32_FIND_DATAW *data): # cannot_raise
33 cdef int mode_bits
34
35@@ -213,25 +204,36 @@
36 def read_dir(self, prefix, top):
37 """Win32 implementation of DirReader.read_dir.
38
39+ :param prefix: A utf8 prefix to be preprended to the path basenames.
40+ :param top: A Unicode or UTF8-encoded path to read.
41+
42+ :return: A list of the directories contents. Each item contains:
43+ (utf8_relpath, utf8_name, kind, lstatvalue, unicode_abspath)
44+
45 :seealso: DirReader.read_dir
46+
47 """
48 cdef WIN32_FIND_DATAW search_data
49 cdef HANDLE hFindFile
50 cdef int last_err
51 cdef WCHAR *query
52 cdef int result
53+ cdef Py_ssize_t length
54
55+ global osutils
56+ if osutils is None:
57+ from . import osutils
58 if prefix:
59- relprefix = prefix + '/'
60+ relprefix = osutils.safe_utf8(prefix) + b'/'
61 else:
62- relprefix = ''
63- top_slash = top + '/'
64+ relprefix = b''
65+ top_slash = osutils.safe_unicode(top) + '/'
66
67 top_star = top_slash + '*'
68
69 dirblock = []
70
71- query = PyUnicode_AS_UNICODE(top_star)
72+ query = PyUnicode_AsWideCharString(top_star, &length)
73 hFindFile = FindFirstFileW(query, &search_data)
74 if hFindFile == INVALID_HANDLE_VALUE:
75 # Raise an exception? This path doesn't seem to exist
76@@ -244,7 +246,7 @@
77 if _should_skip(&search_data):
78 result = FindNextFileW(hFindFile, &search_data)
79 continue
80- name_unicode = _get_name(&search_data)
81+ name_unicode = PyUnicode_FromWideChar(search_data.cFileName, -1)
82 name_utf8 = PyUnicode_AsUTF8String(name_unicode)
83 PyList_Append(dirblock,
84 (relprefix + name_utf8, name_utf8,
85
86=== modified file 'breezy/osutils.py'
87--- breezy/osutils.py 2022-01-14 16:48:37 +0000
88+++ breezy/osutils.py 2022-01-18 07:08:36 +0000
89@@ -24,6 +24,7 @@
90
91 from .lazy_import import lazy_import
92 lazy_import(globals(), """
93+import ctypes
94 from datetime import datetime
95 import getpass
96 import locale
97@@ -1301,7 +1302,7 @@
98 # filesystems), for example, so could probably benefit from the same basic
99 # support there. For now though, only Windows and OSX get that support, and
100 # they get it for *all* file-systems!
101-if sys.platform in ('win32', 'darwin'):
102+if sys.platform in ('win32', 'darwin', 'cygwin'):
103 canonical_relpath = _cicp_canonical_relpath
104 else:
105 canonical_relpath = relpath
106
107=== modified file 'breezy/plugins/upload/tests/test_upload.py'
108--- breezy/plugins/upload/tests/test_upload.py 2021-12-08 18:46:11 +0000
109+++ breezy/plugins/upload/tests/test_upload.py 2022-01-18 07:08:36 +0000
110@@ -109,6 +109,16 @@
111 self.assertFileEqual(content, osutils.pathjoin(base, path))
112
113 def assertUpPathModeEqual(self, path, expected_mode, base=upload_dir):
114+ # FIXME: at present, the upload is tested locally,
115+ # so if local FS doesn't support the mode bits,
116+ # all mode tests will fail. The only mode bit
117+ # that is reported as unsupported by the osutils module
118+ # is the executable bit. So skip if not supports_executable.
119+ # This should better be tested with a dummy transport
120+ # and not an actual file system.
121+ if not osutils.supports_executable(path):
122+ self.skipTest("Cannot determine mode bits for %s" % path)
123+ return
124 # FIXME: the tests needing that assertion should depend on the server
125 # ability to handle chmod so that they don't fail (or be skipped)
126 # against servers that can't. Note that some breezy transports define
127@@ -387,6 +397,9 @@
128 self.assertUpFileEqual(b'bar', 'hello')
129
130 def _test_make_file_executable(self, file_name):
131+ if not osutils.supports_executable(file_name)
132+ self.skipTest("%s cannot be marked executable" % filename)
133+ return
134 self.make_branch_and_working_tree()
135 self.add_file(file_name, b'foo')
136 self.chmod_file(file_name, 0o664)
137
138=== modified file 'breezy/tests/__init__.py'
139--- breezy/tests/__init__.py 2022-01-09 13:12:27 +0000
140+++ breezy/tests/__init__.py 2022-01-18 07:08:36 +0000
141@@ -2654,15 +2654,9 @@
142 """
143 root = TestCaseWithMemoryTransport.TEST_ROOT
144 try:
145- # Make sure we get a readable and accessible home for brz.log
146- # and/or config files, and not fallback to weird defaults (see
147- # http://pad.lv/825027).
148- self.assertIs(None, os.environ.get('BRZ_HOME', None))
149- os.environ['BRZ_HOME'] = root
150 from breezy.bzr.bzrdir import BzrDirMetaFormat1
151 wt = controldir.ControlDir.create_standalone_workingtree(
152 root, format=BzrDirMetaFormat1())
153- del os.environ['BRZ_HOME']
154 except Exception as e:
155 self.fail("Fail to initialize the safety net: %r\n" % (e,))
156 # Hack for speed: remember the raw bytes of the dirstate file so that
157@@ -2696,11 +2690,31 @@
158 suffix='.tmp'))
159 TestCaseWithMemoryTransport.TEST_ROOT = root
160
161+ # Set BRZ_HOME to the test directory so that we never interfere
162+ # with any configuration files existing on the system.
163+ #
164+ # Note that we need both BRZ_HOME AND BZR_HOME:
165+ # BRZ_HOME may be set in the environment and we have to replace it,
166+ # but then bedding doesn't find breezy config in the temporary
167+ # directory, and falls back to BZR_HOME.
168+ #
169+ # This needs to be done before ._create_safety_net is called
170+ # so that the working tree created there doesn't mess with
171+ # the system environment.
172+ self.overrideEnv('BRZ_HOME', root)
173+ self.overrideEnv('BZR_HOME', root)
174+
175 self._create_safety_net()
176
177 # The same directory is used by all tests, and we're not
178 # specifically told when all tests are finished. This will do.
179 atexit.register(_rmtree_temp_dir, root)
180+ else:
181+ # Re-using existing safety net directory;
182+ # isolate the environment (see comment above)
183+ root = TestCaseWithMemoryTransport.TEST_ROOT
184+ self.overrideEnv('BRZ_HOME', root)
185+ self.overrideEnv('BZR_HOME', root)
186
187 self.permit_dir(TestCaseWithMemoryTransport.TEST_ROOT)
188 self.addCleanup(self._check_safety_net)
189@@ -2848,7 +2862,15 @@
190
191 def check_file_contents(self, filename, expect):
192 self.log("check contents of file %s" % filename)
193- with open(filename, 'rb') as f:
194+ # This seems to be called for text files only,
195+ # but most of the existing tests check the contents
196+ # against binary strings. Now there are cases
197+ # when we need to normalize the line endings; expect a string then.
198+ if isinstance(expect, str):
199+ args = {"mode": "rt", "encoding": "utf-8"}
200+ else:
201+ args = {"mode": "rb"}
202+ with open(filename, **args) as f:
203 contents = f.read()
204 if contents != expect:
205 self.log("expected: %r" % expect)
206@@ -4467,11 +4489,9 @@
207 if test_id is not None:
208 ui.ui_factory.clear_term()
209 sys.stderr.write('\nWhile running: %s\n' % (test_id,))
210- # Ugly, but the last thing we want here is fail, so bear with it.
211- printable_e = str(e).decode(osutils.get_user_encoding(), 'replace'
212- ).encode('ascii', 'replace')
213+ # 21-nov-2021 [a1s] Revert revision 5231: in Python3, str has no decode
214 sys.stderr.write('Unable to remove testing dir %s\n%s'
215- % (os.path.basename(dirname), printable_e))
216+ % (os.path.basename(dirname), e))
217
218
219 def probe_unicode_in_user_encoding():
220
221=== modified file 'breezy/tests/test__walkdirs_win32.py'
222--- breezy/tests/test__walkdirs_win32.py 2018-11-12 01:41:38 +0000
223+++ breezy/tests/test__walkdirs_win32.py 2022-01-18 07:08:36 +0000
224@@ -37,9 +37,7 @@
225
226 def setUp(self):
227 super(TestWin32Finder, self).setUp()
228- from ._walkdirs_win32 import (
229- Win32ReadDir,
230- )
231+ from .._walkdirs_win32 import Win32ReadDir
232 self.reader = Win32ReadDir()
233
234 def _remove_stat_from_dirblock(self, dirblock):
235@@ -58,45 +56,44 @@
236 finally:
237 osutils._selected_dir_reader = old_selected_dir_reader
238
239- def assertReadDir(self, expected, prefix, top_unicode):
240+ def assertReadDir(self, expected, prefix, top):
241 result = self._remove_stat_from_dirblock(
242- self.reader.read_dir(prefix, top_unicode))
243+ self.reader.read_dir(prefix, top))
244 self.assertEqual(expected, result)
245
246 def test_top_prefix_to_starting_dir(self):
247 # preparing an iteration should create a unicode native path.
248 self.assertEqual(
249- ('prefix', None, None, None, u'\x12'),
250+ (b'prefix', None, None, None, u'\x12'),
251 self.reader.top_prefix_to_starting_dir(
252 u'\x12'.encode('utf8'), 'prefix'))
253
254 def test_empty_directory(self):
255- self.assertReadDir([], 'prefix', u'.')
256- self.assertWalkdirs([(('', u'.'), [])], u'.')
257+ self.assertReadDir([], b'prefix', b'.')
258+ self.assertWalkdirs([((b'', u'.'), [])], b'.')
259
260 def test_file(self):
261 self.build_tree(['foo'])
262- self.assertReadDir([('foo', 'foo', 'file', u'./foo')],
263+ self.assertReadDir([(b'foo', b'foo', 'file', u'./foo')],
264 '', u'.')
265
266 def test_directory(self):
267 self.build_tree(['bar/'])
268- self.assertReadDir([('bar', 'bar', 'directory', u'./bar')],
269+ self.assertReadDir([(b'bar', b'bar', 'directory', u'./bar')],
270 '', u'.')
271
272 def test_prefix(self):
273 self.build_tree(['bar/', 'baf'])
274 self.assertReadDir([
275- ('xxx/baf', 'baf', 'file', u'./baf'),
276- ('xxx/bar', 'bar', 'directory', u'./bar'),
277+ (b'xxx/baf', b'baf', 'file', u'./baf'),
278+ (b'xxx/bar', b'bar', 'directory', u'./bar'),
279 ],
280 'xxx', u'.')
281
282 def test_missing_dir(self):
283 e = self.assertRaises(WindowsError,
284- self.reader.read_dir, 'prefix', u'no_such_dir')
285- self.assertEqual(errno.ENOENT, e.errno)
286- self.assertEqual(3, e.winerror)
287+ self.reader.read_dir, b'prefix', u'no_such_dir')
288+ # 3 is ERROR_PATH_NOT_FOUND, see WinError.h
289 self.assertEqual((3, u'no_such_dir/*'), e.args)
290
291
292@@ -106,7 +103,7 @@
293
294 def setUp(self):
295 super(Test_Win32Stat, self).setUp()
296- from ._walkdirs_win32 import lstat
297+ from .._walkdirs_win32 import lstat
298 self.win32_lstat = lstat
299
300 def test_zero_members_present(self):
301
302=== modified file 'breezy/tests/test_bedding.py'
303--- breezy/tests/test_bedding.py 2022-01-14 00:23:44 +0000
304+++ breezy/tests/test_bedding.py 2022-01-18 07:08:36 +0000
305@@ -43,11 +43,17 @@
306 self.overrideEnv('HOME', '/home/bogus')
307 self.overrideEnv('XDG_CACHE_HOME', '')
308 if sys.platform == 'win32':
309- self.overrideEnv(
310- 'BRZ_HOME',
311- r'C:\Documents and Settings\bogus\Application Data')
312- self.brz_home = \
313- 'C:/Documents and Settings/bogus/Application Data/breezy'
314+ # On Windows, the search sequence is:
315+ # 1. BRZ_HOME if breezy config exists or else
316+ # if there's no existing bazaar config.
317+ # 2. AppData\Roaming (from special shell folders if possible;
318+ # if not, then from APPDATA environment variable.)
319+ # The HOME environment variable (set above) is never checked.
320+ # Let's take BRZ_HOME, as it is the first.
321+ # (Also disable BZR_HOME to not find existing bazaar config.)
322+ self.overrideEnv('BRZ_HOME', r'C:\Users\bogus\AppData')
323+ self.overrideEnv('BZR_HOME', r'C:\Users\bogus\AppData')
324+ self.brz_home = 'C:/Users/bogus/AppData/breezy'
325 else:
326 self.brz_home = '/home/bogus/.config/breezy'
327
328@@ -73,11 +79,19 @@
329 class TestConfigPathFallback(tests.TestCaseInTempDir):
330
331 def setUp(self):
332+ if sys.platform == 'win32':
333+ raise tests.TestNotApplicable(
334+ 'Fallback config paths are different on this platform')
335 super(TestConfigPathFallback, self).setUp()
336 self.overrideEnv('HOME', self.test_dir)
337 self.overrideEnv('XDG_CACHE_HOME', '')
338 self.bzr_home = os.path.join(self.test_dir, '.bazaar')
339 os.mkdir(self.bzr_home)
340+ # The safety net made by super() has set BZR_HOME and BRZ_HOME to the
341+ # temporary root shared by all tests. As they take precedence, we need
342+ # to erase the variables in order to check the HOME created here.
343+ self.overrideEnv('BRZ_HOME', None)
344+ self.overrideEnv('BZR_HOME', None)
345
346 def test_config_dir(self):
347 self.assertEqual(bedding.config_dir(), self.bzr_home)
348@@ -192,6 +206,15 @@
349 class TestDefaultMailDomain(tests.TestCaseInTempDir):
350 """Test retrieving default domain from mailname file"""
351
352+ def setUp(self):
353+ if sys.platform == 'win32':
354+ # The function deliberately returns None on Windows.
355+ # Could run C:\Windows\System32\whoami.exe /upn, but that
356+ # only works when the user is logged in to an AD domain.
357+ # Why we don't just read /etc/maildomain, same as on all platforms?
358+ raise tests.TestNotApplicable
359+ super(TestDefaultMailDomain, self).setUp()
360+
361 def test_default_mail_domain_simple(self):
362 with open('simple', 'w') as f:
363 f.write("domainname.com\n")
364
365=== modified file 'breezy/tests/test_config.py'
366--- breezy/tests/test_config.py 2020-06-23 01:02:30 +0000
367+++ breezy/tests/test_config.py 2022-01-18 07:08:36 +0000
368@@ -1012,12 +1012,12 @@
369 """Creating a new entry in config uses a local path."""
370 branch = self.make_branch('branch', format='knit')
371 branch.set_push_location('http://foobar')
372- local_path = osutils.getcwd().encode('utf8')
373+ local_path = osutils.getcwd()
374 # Surprisingly ConfigObj doesn't create a trailing newline
375 self.check_file_contents(bedding.locations_config_path(),
376- b'[%s/branch]\n'
377- b'push_location = http://foobar\n'
378- b'push_location:policy = norecurse\n'
379+ '[%s/branch]\n'
380+ 'push_location = http://foobar\n'
381+ 'push_location:policy = norecurse\n'
382 % (local_path,))
383
384 def test_autonick_urlencoded(self):
385@@ -3190,11 +3190,22 @@
386
387 def test_branch_name_basename(self):
388 store = self.get_store(self)
389- store._load_from_string(dedent("""\
390- [/]
391- push_location=my{branchname}
392- """).encode('ascii'))
393- matcher = config.LocationMatcher(store, 'file:///parent/example%3c')
394+ if sys.platform == "win32":
395+ # Win32 file urls start with file:///x:/,
396+ # where x is a valid drive letter
397+ store._load_from_string(dedent("""\
398+ [C:/]
399+ push_location=my{branchname}
400+ """).encode('ascii'))
401+ matcher = config.LocationMatcher(store,
402+ 'file:///c:/parent/example%3c')
403+ else:
404+ store._load_from_string(dedent("""\
405+ [/]
406+ push_location=my{branchname}
407+ """).encode('ascii'))
408+ matcher = config.LocationMatcher(store,
409+ 'file:///parent/example%3c')
410 self.assertEqual('example<', matcher.branch_name)
411 ((_, section),) = matcher.get_sections()
412 self.assertEqual('example<', section.locals['branchname'])
413@@ -3220,19 +3231,31 @@
414
415 def test_url_vs_local_paths(self):
416 # The matcher location is an url and the section names are local paths
417- self.assertSectionIDs(['/foo/bar', '/foo'],
418- 'file:///foo/bar/baz', b'''\
419-[/foo]
420-[/foo/bar]
421-''')
422+ if sys.platform == "win32":
423+ # Win32 file urls start with file:///x:/,
424+ # where x is a valid drive letter
425+ self.assertSectionIDs(['C:/foo/bar', 'C:/foo'],
426+ 'file:///c:/foo/bar/baz',
427+ b'[C:/foo]\n'
428+ b'[C:/foo/bar]\n')
429+ else:
430+ self.assertSectionIDs(['/foo/bar', '/foo'],
431+ 'file:///foo/bar/baz',
432+ b'[/foo]\n'
433+ b'[/foo/bar]\n')
434
435 def test_local_path_vs_url(self):
436 # The matcher location is a local path and the section names are urls
437- self.assertSectionIDs(['file:///foo/bar', 'file:///foo'],
438- '/foo/bar/baz', b'''\
439-[file:///foo]
440-[file:///foo/bar]
441-''')
442+ if sys.platform == "win32":
443+ self.assertSectionIDs(['file:///C:/foo/bar', 'file:///C:/foo'],
444+ 'C:/foo/bar/baz',
445+ b'[file:///C:/foo]\n'
446+ b'[file:///C:/foo/bar]\n')
447+ else:
448+ self.assertSectionIDs(['file:///foo/bar', 'file:///foo'],
449+ '/foo/bar/baz',
450+ b'[file:///foo]\n'
451+ b'[file:///foo/bar]\n')
452
453 def test_no_name_section_included_when_present(self):
454 # Note that other tests will cover the case where the no-name section
455
456=== modified file 'breezy/tests/test_osutils.py'
457--- breezy/tests/test_osutils.py 2022-01-16 17:48:26 +0000
458+++ breezy/tests/test_osutils.py 2022-01-18 07:08:36 +0000
459@@ -876,8 +876,10 @@
460 self.requireFeature(features.win32_feature)
461 self.assertEqual('C:/foo', osutils._win32_abspath('C:\\foo'))
462 self.assertEqual('C:/foo', osutils._win32_abspath('C:/foo'))
463- self.assertEqual('//HOST/path', osutils._win32_abspath(r'\\HOST\path'))
464- self.assertEqual('//HOST/path', osutils._win32_abspath('//HOST/path'))
465+ self.assertEqual('//HOST/SHARE/path',
466+ osutils._win32_abspath(r'\\HOST\share\path'))
467+ self.assertEqual('//HOST/SHARE/path',
468+ osutils._win32_abspath('//host/SHARE/path'))
469
470 def test_realpath(self):
471 self.assertEqual('C:/foo', osutils._win32_realpath('C:\\foo'))
472@@ -1068,10 +1070,30 @@
473
474 class TestWalkDirs(tests.TestCaseInTempDir):
475
476+ if sys.platform == 'win32':
477+ @staticmethod
478+ def _normalize_path_separators(path):
479+ return path.replace("\\", "/")
480+ else:
481+ @staticmethod
482+ def _normalize_path_separators(path):
483+ return path
484+
485 def assertExpectedBlocks(self, expected, result):
486- self.assertEqual(expected,
487- [(dirinfo, [line[0:3] for line in block])
488- for dirinfo, block in result])
489+ # This is randomly called with result obtained from _walkdirs_utf8
490+ # which always uses forward slashes for path separators,
491+ # and simple walkdirs, which relies on os.scandir,
492+ # so yields paths with backslashes on Windows.
493+ # We don't compare the "path-from-top" members here,
494+ # but in deeper trees it appears in the "directory-path-from-top".
495+ # We have to normalize the slashes in that element.
496+ _normalize_walkdirs_result = lambda spec: [(
497+ (relpath, self._normalize_path_separators(fromtop)),
498+ [line[:3] for line in block],
499+ ) for ((relpath, fromtop), block) in spec]
500+ self.assertEqual(
501+ _normalize_walkdirs_result(expected),
502+ _normalize_walkdirs_result(result))
503
504 def test_walkdirs(self):
505 tree = [
506@@ -1223,13 +1245,23 @@
507 self.assertExpectedBlocks(expected_dirblocks[1:], result)
508
509 def _filter_out_stat(self, result):
510- """Filter out the stat value from the walkdirs result"""
511- for dirdetail, dirblock in result:
512+ """Filter out the stat value from the walkdirs result
513+
514+ If needed, also change the directory separators
515+ in the path-from-top elements to be forward slashes.
516+
517+ """
518+ new_result = []
519+ for ((relpath, fromtop), dirblock) in result:
520 new_dirblock = []
521 for info in dirblock:
522 # Ignore info[3] which is the stat
523- new_dirblock.append((info[0], info[1], info[2], info[4]))
524- dirblock[:] = new_dirblock
525+ new_dirblock.append((info[0], info[1], info[2],
526+ self._normalize_path_separators(info[4])))
527+ new_result.append((
528+ (relpath, self._normalize_path_separators(fromtop)),
529+ new_dirblock))
530+ return new_result
531
532 def _save_platform_info(self):
533 self.overrideAttr(osutils, '_fs_enc')
534@@ -1257,6 +1289,8 @@
535 UTF8DirReaderFeature.module.UTF8DirReader, b".")
536
537 def test_force_walkdirs_utf8_fs_latin1(self):
538+ if sys.platform == "win32":
539+ self.skipTest("On Windows, FS functions use wide chars")
540 self._save_platform_info()
541 osutils._fs_enc = 'iso-8859-1'
542 self.assertDirReaderIs(osutils.UnicodeDirReader, ".")
543@@ -1301,11 +1335,9 @@
544 ]
545 ),
546 ]
547- result = list(osutils.walkdirs('.'))
548- self._filter_out_stat(result)
549+ result = self._filter_out_stat(osutils.walkdirs('.'))
550 self.assertEqual(expected_dirblocks, result)
551- result = list(osutils.walkdirs(u'./' + name1, name1))
552- self._filter_out_stat(result)
553+ result = self._filter_out_stat(osutils.walkdirs(u'./' + name1, name1))
554 self.assertEqual(expected_dirblocks[1:], result)
555
556 def test_unicode__walkdirs_utf8(self):
557@@ -1415,8 +1447,7 @@
558 ]
559 ),
560 ]
561- result = list(osutils._walkdirs_utf8('.'))
562- self._filter_out_stat(result)
563+ result = self._filter_out_stat(osutils._walkdirs_utf8('.'))
564 self.assertEqual(expected_dirblocks, result)
565
566 def test__walkdirs_utf8_win32readdir(self):
567@@ -1443,26 +1474,25 @@
568 # All of the abspaths should be in unicode, all of the relative paths
569 # should be in utf8
570 expected_dirblocks = [
571- (('', '.'),
572+ ((b'', '.'),
573 [(name0, name0, 'file', './' + name0u),
574 (name1, name1, 'directory', './' + name1u),
575 (name2, name2, 'file', './' + name2u),
576 ]
577 ),
578 ((name1, './' + name1u),
579- [(name1 + '/' + name0, name0, 'file', './' + name1u
580+ [(name1 + b'/' + name0, name0, 'file', './' + name1u
581 + '/' + name0u),
582- (name1 + '/' + name1, name1, 'directory', './' + name1u
583+ (name1 + b'/' + name1, name1, 'directory', './' + name1u
584 + '/' + name1u),
585 ]
586 ),
587- ((name1 + '/' + name1, './' + name1u + '/' + name1u),
588+ ((name1 + b'/' + name1, './' + name1u + '/' + name1u),
589 [
590 ]
591 ),
592 ]
593- result = list(osutils._walkdirs_utf8(u'.'))
594- self._filter_out_stat(result)
595+ result = self._filter_out_stat(osutils._walkdirs_utf8(u'.'))
596 self.assertEqual(expected_dirblocks, result)
597
598 def assertStatIsCorrect(self, path, win32stat):
599@@ -1471,8 +1501,10 @@
600 self.assertAlmostEqual(os_stat.st_mtime, win32stat.st_mtime, places=4)
601 self.assertAlmostEqual(os_stat.st_ctime, win32stat.st_ctime, places=4)
602 self.assertAlmostEqual(os_stat.st_atime, win32stat.st_atime, places=4)
603- self.assertEqual(os_stat.st_dev, win32stat.st_dev)
604- self.assertEqual(os_stat.st_ino, win32stat.st_ino)
605+ # win32stat is built from WIN32_FIND_DATA structure
606+ # which contains neither dwVolumeSerialNumber nor nFileIndex
607+ #self.assertEqual(os_stat.st_dev, win32stat.st_dev)
608+ #self.assertEqual(os_stat.st_ino, win32stat.st_ino)
609 self.assertEqual(os_stat.st_mode, win32stat.st_mode)
610
611 def test__walkdirs_utf_win32_find_file_stat_file(self):
612@@ -1489,7 +1521,7 @@
613 with open(name0u, 'ab') as f:
614 f.write(b'just a small update')
615
616- result = Win32ReadDir().read_dir('', u'.')
617+ result = Win32ReadDir().read_dir(b'', u'.')
618 entry = result[0]
619 self.assertEqual((name0, name0, 'file'), entry[:3])
620 self.assertEqual(u'./' + name0u, entry[4])
621@@ -1617,13 +1649,17 @@
622 processed_links = []
623
624 def file_handler(from_path, to_path):
625- processed_files.append(('f', from_path, to_path))
626+ processed_files.append(
627+ ('f', osutils.normpath(from_path), osutils.normpath(to_path)))
628
629 def dir_handler(from_path, to_path):
630- processed_files.append(('d', from_path, to_path))
631+ processed_files.append(
632+ ('d', osutils.normpath(from_path), osutils.normpath(to_path)))
633
634 def link_handler(from_path, to_path):
635- processed_links.append((from_path, to_path))
636+ processed_links.append(
637+ (osutils.normpath(from_path), osutils.normpath(to_path)))
638+
639 handlers = {'file': file_handler,
640 'directory': dir_handler,
641 'symlink': link_handler,
642
643=== modified file 'breezy/transport/local.py'
644--- breezy/transport/local.py 2022-01-12 23:43:40 +0000
645+++ breezy/transport/local.py 2022-01-18 07:08:36 +0000
646@@ -30,13 +30,12 @@
647
648 from breezy import (
649 atomicfile,
650- osutils,
651 urlutils,
652 )
653 from breezy.transport import LateReadError
654 """)
655
656-from .. import transport
657+from .. import osutils, transport
658
659
660 _append_flags = os.O_CREAT | os.O_APPEND | os.O_WRONLY | osutils.O_BINARY | osutils.O_NOINHERIT
661@@ -154,7 +153,10 @@
662 path = self._abspath(relpath)
663 return open(path, 'rb')
664 except (IOError, OSError) as e:
665- if e.errno == errno.EISDIR:
666+ if e.errno == errno.EISDIR or (
667+ (sys.platform == "win32") and (e.errno == errno.EACCES)
668+ and os.path.isdir(path)
669+ ):
670 return LateReadError(relpath)
671 self._translate_error(e, path)
672

Subscribers

People subscribed via source and target branches