Skip to content

Commit 8fe727b

Browse files
authored
Tests for diod (#5)
* Register scheme "p9" in fsspec. * Fix test for Python 3.11 String representation of Enum value includes Enum class in 3.11. Use `value` to get a string value of the Enum. * Release 0.0.3 * Tests for diod * Install for root * Release 0.0.4
1 parent ab4cc33 commit 8fe727b

File tree

8 files changed

+112
-38
lines changed

8 files changed

+112
-38
lines changed

.github/workflows/test.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ jobs:
4242
cache: pip
4343
cache-dependency-path: pyproject.toml
4444

45+
- name: Install diod
46+
run: |
47+
sudo apt update -y
48+
sudo apt install -y diod
49+
4550
- name: Install dependencies
4651
run: |
4752
python -m pip install --upgrade pip
@@ -50,3 +55,8 @@ jobs:
5055
- name: Run tests
5156
run: ./scripts/test.sh
5257

58+
- name: Run tests for diod
59+
run: |
60+
sudo python -m pip install --upgrade pip
61+
sudo python -m pip install -e .[tests]
62+
sudo ./scripts/test.sh --diod

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ Supported protocols:
88
* [9P2000.u](https://ericvh.github.io/9p-rfc/rfc9p2000.u.html)
99
* [9P2000.L](https://github.com/chaos/diod/blob/master/protocol.md)
1010

11-
Supported servers:
11+
Tested with the following servers:
1212

1313
* [py9p](https://github.com/pbchekin/p9fs-py/blob/main/src/py9p/__main__.py)
1414
* [unpfs](https://github.com/pfpacket/rust-9p/blob/master/README.md#unpfs)
15+
* [diod](https://github.com/chaos/diod)
1516

1617
## Examples
1718

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "p9fs"
3-
version = "0.0.3"
3+
version = "0.0.4"
44
description = "9P implementation of Python fsspec"
55
license = {file = "LICENSE"}
66
readme = "README.md"

scripts/test.sh

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ PY9P_LOG=/tmp/py9p.log
1313
UNPFS_PID=/tmp/unpfs.pid
1414
UNPFS_LOG=/tmp/unpfs.log
1515

16+
DIOD_PID=/tmp/diod.pid
17+
DIOD_LOG=/tmp/diod.log
18+
1619
export PYTHONUNBUFFERED=1
1720

1821
function mymktempdir {
@@ -78,12 +81,29 @@ function stop_unpfs {
7881
fi
7982
}
8083

84+
function run_diod {
85+
local exported_dir="$1"
86+
${DIOD:-diod} -f -l 0.0.0.0:1234 -e "$exported_dir" -d3 -n &> $DIOD_LOG &
87+
echo $! > $DIOD_PID
88+
wait_for_server
89+
}
90+
91+
function stop_diod {
92+
if [[ -f $DIOD_PID ]]; then
93+
kill "$(<$DIOD_PID)" || true
94+
rm -f "$DIOD_PID"
95+
fi
96+
}
97+
8198
function user_tests {
82-
cd "$PROJECT_ROOT/src"
83-
python -m pytest -v -rA ../tests/user "$@"
99+
cd "$PROJECT_ROOT"
100+
python -m pytest -v -rA tests/user "$@"
84101
}
85102

86103
function cleanup {
104+
stop_py9p
105+
stop_unpfs
106+
stop_diod
87107
if [[ -f $TMP_DIRS_FILE ]]; then
88108
xargs -n1 -r rm -rf < $TMP_DIRS_FILE
89109
rm -f $TMP_DIRS_FILE
@@ -96,20 +116,29 @@ rm -f "$TMP_DIRS_FILE"
96116

97117
echo "PROJECT_ROOT: $PROJECT_ROOT"
98118

99-
echo "Testing py9p server with 9P2000"
100-
EXPORTED_DIR=""$(mymktempdir)""
101-
run_py9p "$EXPORTED_DIR" 9P2000
102-
user_tests --exported "$EXPORTED_DIR" --9p 9P2000
103-
stop_py9p
104-
105-
echo "Testing py9p server with 9P2000.u"
106-
EXPORTED_DIR=""$(mymktempdir)""
107-
run_py9p "$EXPORTED_DIR" 9P2000.u
108-
user_tests --exported "$EXPORTED_DIR" --9p 9P2000.u
109-
stop_py9p
110-
111-
echo "Testing unpfs server with 9P2000.L"
112-
EXPORTED_DIR=""$(mymktempdir)""
113-
run_unpfs "$EXPORTED_DIR"
114-
user_tests --exported "$EXPORTED_DIR" --9p 9P2000.L
115-
stop_unpfs
119+
# Diod requires root, so running it conditionally
120+
if [[ " $@ " =~ " --diod " ]]; then
121+
echo "Testing diod server with 9P2000.L"
122+
EXPORTED_DIR=""$(mymktempdir)""
123+
run_diod "$EXPORTED_DIR"
124+
user_tests --exported "$EXPORTED_DIR" --9p 9P2000.L --aname "$EXPORTED_DIR"
125+
stop_diod
126+
else
127+
echo "Testing py9p server with 9P2000"
128+
EXPORTED_DIR=""$(mymktempdir)""
129+
run_py9p "$EXPORTED_DIR" 9P2000
130+
user_tests --exported "$EXPORTED_DIR" --9p 9P2000
131+
stop_py9p
132+
133+
echo "Testing py9p server with 9P2000.u"
134+
EXPORTED_DIR=""$(mymktempdir)""
135+
run_py9p "$EXPORTED_DIR" 9P2000.u
136+
user_tests --exported "$EXPORTED_DIR" --9p 9P2000.u
137+
stop_py9p
138+
139+
echo "Testing unpfs server with 9P2000.L"
140+
EXPORTED_DIR=""$(mymktempdir)""
141+
run_unpfs "$EXPORTED_DIR"
142+
user_tests --exported "$EXPORTED_DIR" --9p 9P2000.L
143+
stop_unpfs
144+
fi

src/p9fs/__init__.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import errno
6+
import io
67
import os
78
import pathlib
89
import socket
@@ -44,13 +45,6 @@ class P9FileSystem(fsspec.AbstractFileSystem):
4445

4546
protocol = '9p'
4647

47-
host: str
48-
port: int
49-
username: str
50-
password: Optional[str]
51-
version: Version
52-
verbose: bool
53-
5448
def __init__(
5549
self,
5650
host: str,
@@ -59,6 +53,7 @@ def __init__(
5953
password: Optional[str] = None,
6054
version: Union[str, Version] = Version.v9P2000L,
6155
verbose: bool = False,
56+
aname: str = '',
6257
**kwargs,
6358
):
6459
"""9P implementation of fsspec.
@@ -80,6 +75,7 @@ def __init__(
8075
else:
8176
self.version = version
8277
self.verbose = verbose
78+
self.aname = aname
8379
self.fids = fid.FidCache()
8480
self._rlock = threading.RLock()
8581
super().__init__(**kwargs)
@@ -107,7 +103,13 @@ def _connect(self):
107103
s = socket.socket(socket.AF_INET)
108104
s.connect((self.host, self.port))
109105
credentials = py9p.Credentials(user=self.username, passwd=self.password)
110-
self.client = py9p.Client(s, credentials=credentials, ver=self.version, chatty=self.verbose)
106+
self.client = py9p.Client(
107+
s,
108+
credentials=credentials,
109+
ver=self.version,
110+
chatty=self.verbose,
111+
aname=self.aname,
112+
)
111113

112114
def _mkdir(self, path, mode: int = 0o755):
113115
self._mknod(path, mode | stat.S_IFDIR)
@@ -128,7 +130,12 @@ def _mknod(self, tfid, path, mode):
128130
@with_fid
129131
def _info(self, tfid, path):
130132
parts = pathlib.Path(path).parts
131-
self.client._walk(self.client.ROOT, tfid, parts)
133+
response = self.client._walk(self.client.ROOT, tfid, parts)
134+
# canonical 9p implementation and specifically diod do not return an error if walk is
135+
# unsuccessful. Instead, they return all qids up to last existing component in the path.
136+
# The only way to check if path exists is compare the number of qids in the response.
137+
if len(response.wqid) != len(parts):
138+
raise P9FileNotFound(path)
132139
if self.version == Version.v9P2000L:
133140
response = self.client._getattr(tfid)
134141
self.client._clunk(tfid)
@@ -180,7 +187,9 @@ def _info_from_rstat(self, parent: str, response) -> List[Dict]:
180187

181188
@with_fid
182189
def _unlink(self, tfid, path):
183-
if self.version == Version.v9P2000L:
190+
# TODO: diod does not support unlinkat, remove should be used instead.
191+
# Currently the only way to identify diod is to check if aname is set.
192+
if self.version == Version.v9P2000L and not self.aname:
184193
p = pathlib.Path(path)
185194
self.client._walk(self.client.ROOT, tfid, p.parent.parts)
186195
self.client._unlinkat(tfid, p.name)
@@ -227,7 +236,7 @@ def _readdir(self, tfid, path):
227236
items = []
228237
offset = 0
229238
while True:
230-
response = self.client._readdir(tfid, offset, self.client.msize)
239+
response = self.client._readdir(tfid, offset, self.client.msize - py9p.IOHDRSZ)
231240
if response.count == 0:
232241
break
233242
for entry in response.stat:
@@ -433,7 +442,14 @@ def _upload_chunk(self, final=False):
433442
self.closed = True
434443
raise
435444

436-
self.fs._write(self.buffer.getvalue(), self.offset, self._f)
445+
data = self.buffer.getvalue()
446+
size = len(data)
447+
offset = 0
448+
msize = self.fs.client.msize - py9p.IOHDRSZ
449+
while offset < size - 1:
450+
asize = min(msize, size - offset)
451+
self.fs._write(data[offset:offset + asize], self.offset + offset, self._f)
452+
offset += asize
437453
return True
438454

439455
def _initiate_upload(self):
@@ -446,7 +462,13 @@ def _fetch_range(self, start, end):
446462
"""Get the specified set of bytes from remote"""
447463
if self._f is None:
448464
self._f = self.fs._open_fid(self.path, os.O_RDONLY)
449-
return self.fs._read(end - start + 1, start, self._f)
465+
data = io.BytesIO()
466+
offset = start
467+
msize = self.fs.client.msize - py9p.IOHDRSZ
468+
while offset < end:
469+
asize = min(msize, end - offset + 1)
470+
offset += data.write(self.fs._read(asize, offset, self._f))
471+
return data.getvalue()
450472

451473
def close(self):
452474
"""Close file"""

src/py9p/py9p.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,12 +1584,13 @@ def __init__(
15841584
chatty=0,
15851585
ver: Version = Version.v9P2000,
15861586
msize=8192,
1587+
aname='',
15871588
):
15881589
self.credentials = credentials
15891590
self.version = ver
15901591
self.msize = msize
15911592
self.fd = Sock(fd, self.dotu, chatty)
1592-
self.login(authsrv, credentials)
1593+
self.login(authsrv, credentials, aname)
15931594

15941595
@property
15951596
def dotu(self):
@@ -1758,7 +1759,7 @@ def _fullclose(self):
17581759
self._clunk(self.CWD)
17591760
self.fd.close()
17601761

1761-
def login(self, authsrv, credentials):
1762+
def login(self, authsrv, credentials, aname=''):
17621763
ver = self.version.to_bytes()
17631764
fcall = self._version(self.msize, ver)
17641765
self.msize = fcall.msize
@@ -1785,7 +1786,9 @@ def login(self, authsrv, credentials):
17851786
else:
17861787
raise ClientError('unknown authentication method: %s' % credentials.authmode)
17871788

1788-
self._attach(self.ROOT, fcall.afid, credentials.user, b'')
1789+
if isinstance(aname, str):
1790+
aname = aname.encode('utf-8')
1791+
self._attach(self.ROOT, fcall.afid, credentials.user, aname)
17891792
if fcall.afid != NOFID:
17901793
self._clunk(fcall.afid)
17911794
self._walk(self.ROOT, self.CWD, [])

tests/user/conftest.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,10 @@ def pytest_addoption(parser):
4343
default=False,
4444
help="Verbose 9P client",
4545
)
46+
parser.addoption(
47+
"--aname",
48+
action="store",
49+
default="",
50+
type=str,
51+
help="Name to attach to, required for diod",
52+
)

tests/user/test_p9fs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
Examples:
88
unpfs 'tcp!0.0.0.0!1234' $TMPDIR
99
python -m py9p -p 1234 -w -r $TMPDIR
10+
sudo diod -f -l 0.0.0.0:1234 -d3 -n -e $TMPDIR
1011
1112
Pass this TMPDIR to the test suite with `--exported`:
1213
python -m pytest tests/user --exported $TMPDIR
@@ -29,6 +30,7 @@ def fs(pytestconfig):
2930
username=pytestconfig.getoption('--user'),
3031
version=pytestconfig.getoption('--9p'),
3132
verbose=pytestconfig.getoption('--chatty'),
33+
aname=pytestconfig.getoption('--aname')
3234
)
3335

3436

@@ -156,7 +158,7 @@ def test_copy(fs, exported_path):
156158

157159

158160
def test_registration(fs, exported_path):
159-
url = f'p9://{fs.username}@{fs.host}:{fs.port}/xxx?version={fs.version.value}'
161+
url = f'p9://{fs.username}@{fs.host}:{fs.port}/xxx?version={fs.version.value}&aname={fs.aname}'
160162
with fsspec.open(url, 'r') as f:
161163
data = f.read()
162164
assert data == 'This is a test content'

0 commit comments

Comments
 (0)