Skip to content

Commit

Permalink
introduce frontmatter, provide env and examples in .ispec file directly
Browse files Browse the repository at this point in the history
  • Loading branch information
luto committed Jul 10, 2024
1 parent 66346dc commit a18e2e9
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 186 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ classifiers = [
dependencies = [
"pexpect==4.8.0",
"termcolor==2.2.0",
"PyYAML==6.0.1",
]
dynamic = ["version"]

Expand Down
11 changes: 6 additions & 5 deletions src/shellinspector/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ def parse_spec_file(path):

if not spec_file.exists():
LOGGER.error("file %s does not exist", spec_file)
return False
return None

lines = spec_file.read_text().splitlines()
specfile = parse(spec_file, lines)
with open(spec_file) as f:
specfile = parse(spec_file, f)

for i, command in enumerate(specfile.commands):
LOGGER.debug("command[%s]: %s", i, command.short)
Expand All @@ -64,8 +64,6 @@ def parse_spec_file(path):
error.message,
)

return False

return specfile


Expand All @@ -90,6 +88,9 @@ def run(target_host, spec_file_paths, identity, verbose):
for spec_file_path in spec_file_paths:
spec_file = parse_spec_file(spec_file_path)

if spec_file is None or spec_file.errors:
continue

if spec_file.examples:
for example in spec_file.examples:
spec_files.append(spec_file.as_example(example))
Expand Down
125 changes: 29 additions & 96 deletions src/shellinspector/parser.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import re
import typing
from dataclasses import dataclass
from dataclasses import replace
from enum import Enum
from pathlib import Path

import yaml


class ExecutionMode(Enum):
USER = "$"
Expand Down Expand Up @@ -111,109 +114,39 @@ def as_example(self, example):
)


def parse_env(path, lines: list[str]):
errors = []
environment = {}

for line_no, line in enumerate(lines, 1):
try:
if line.startswith("#") or not line.strip():
continue

k, _, v = line.partition("=")
k = k.strip()
v = v.strip()

if not v:
errors.append(
Error(
path,
line_no,
line,
"line has no value",
)
)
continue

environment[k] = v
except Exception as ex:
errors.append(
Error(
path,
line_no,
line,
str(ex),
)
)

return environment, errors


def parse_examples(path, lines: list[str]):
errors = []
keys = None
examples = []

if len(lines) <= 1:
return examples
def parse_yaml_multidoc(stream: typing.IO) -> tuple[str, str]:
if stream.read(3) != "---":
stream.seek(0)
return {}, stream.read()
else:
stream.seek(0)

for line_no, line in enumerate(lines, 1):
if line.startswith("#") or not line.strip():
continue

if keys is None:
keys = line.split()
continue
loader = yaml.SafeLoader(stream)

try:
values = line.split()

if len(values) != len(keys):
errors.append(
Error(
path,
line_no,
line,
f"Number of values ({len(values)}) does not match"
f"number of keys in header ({len(keys)})",
)
)
continue
try:
# load the 1st document (up to '---') as yaml ...
frontmatter = loader.get_data()
# ... and the rest as plain text, to be parsed later, minus \x00 at the
# end added by the yaml parser as EOF
tests = loader.buffer[loader.pointer + 1 : -1]
finally:
loader.dispose()

examples.append(dict(zip(keys, values)))
except Exception as ex:
errors.append(
Error(
path,
line_no,
line,
str(ex),
)
)
if frontmatter is None:
frontmatter = {}

return examples, errors
return frontmatter, tests


def parse(path: str, lines: list[str]) -> Specfile:
def parse(path: str, stream: typing.IO) -> Specfile:
specfile = Specfile(path)

env_path = specfile.path.with_suffix(".ispec.env")
frontmatter, tests = parse_yaml_multidoc(stream)

if env_path.exists():
environment, errors = parse_env(env_path, env_path.read_text().splitlines())
specfile.environment.update(environment)
specfile.errors.extend(errors)
specfile.examples = frontmatter.get("examples", [])
specfile.environment = frontmatter.get("environment", {})

examples_path = specfile.path.with_suffix(".ispec.examples")

if examples_path.exists():
examples, errors = parse_examples(
examples_path, examples_path.read_text().splitlines()
)
specfile.examples = examples
specfile.errors.extend(errors)

for line_no, line in enumerate(lines, 1):
for line_no, line in enumerate(tests.splitlines(), 1):
# comment
if line.startswith("#"):
continue
Expand Down Expand Up @@ -246,9 +179,9 @@ def parse(path: str, lines: list[str]) -> Specfile:
)
)
else:
included_specfile = parse(
include_path, include_path.read_text().splitlines()
)
with open(include_path) as f:
included_specfile = parse(include_path, f)

specfile.errors.extend(included_specfile.errors)
specfile.commands.extend(included_specfile.commands)

Expand Down
2 changes: 2 additions & 0 deletions tests/data/test.ispec
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
---
---
% whoami
root
2 changes: 2 additions & 0 deletions tests/data/test_error.ispec
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
---
---
a
% ls2
b
8 changes: 6 additions & 2 deletions tests/e2e/500_envtest.ispec
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
---
environment:
something: else
number: 1
number: 2
---
[@local]$ echo $something
else
[@local]$ echo $number
2
[@local]$ echo $quotetest
'
8 changes: 0 additions & 8 deletions tests/e2e/500_envtest.ispec.env

This file was deleted.

7 changes: 7 additions & 0 deletions tests/e2e/600_examplestest.ispec
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
---
examples:
- PY_COMMAND: "python3.10"
PY_VERSION: "3.10"
- PY_COMMAND: "python3.11"
PY_VERSION: "3.11"
---
[@local]$ echo Python {PY_COMMAND} --version | grep -Po '[0-9.]+'
{PY_VERSION}
3 changes: 0 additions & 3 deletions tests/e2e/600_examplestest.ispec.examples

This file was deleted.

Loading

0 comments on commit a18e2e9

Please sign in to comment.