Skip to content

Commit 0d45d06

Browse files
- Actually works.
1 parent 6d46f19 commit 0d45d06

File tree

2 files changed

+92
-7
lines changed

2 files changed

+92
-7
lines changed

docs/walkthroughs/get-google-vms.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,23 @@ WHERE
3636

3737
You will see something very much like this included in the output, presuming you have one VM (if you have zero, only the headers should appper, more VMs means more rows):
3838

39-
```sql stackql stdout table-contains-data
39+
```sql stackql stdout expectation stdout-table-contains-data
4040
|--------------------------------------------------|---------------------|
4141
| name | id |
4242
|--------------------------------------------------|---------------------|
4343
| any-compute-cluster-1-default-abcd-00000001-0001 | 1000000000000000001 |
4444
|--------------------------------------------------|---------------------|
4545
```
4646

47-
<!--- STDERR_REGEX_EXACT
47+
<!--- EXPECTATION
4848
google\ provider,\ version\ 'v24.11.00274'\ successfully\ installed
4949
goodbye
5050
-->
5151

52+
<x-expectation style="display: none;">
53+
<stdout-contains-nonempty-table></stdout-contains-nonempty-table>
54+
</x-expectation>
55+
5256
## Cleanup
5357

5458
```bash teardown best-effort app-root-path=./test/tmp/.get-google-vms.stackql

test/python/markdown_testing/markdown_testing.py

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ class ASTNode(object):
2121
_BASH: str = 'bash'
2222
_SETUP: str = 'setup'
2323
_TEARDOWN: str = 'teardown'
24+
_BLOCK_CODE: str = 'block_code'
25+
_EXPECTATION: str = 'expectation'
26+
_STDOUT: str = 'stdout'
27+
_STDERR: str = 'stderr'
28+
_TABLE_CONTAINNS_DATA: str = 'table-contains-data'
29+
_REGEX: str = 'regex'
2430

2531
def __init__(self, node: dict):
2632
self.node = node
@@ -41,7 +47,19 @@ def get_text(self) -> str:
4147
return self.node.get('raw', '').strip()
4248

4349
def is_executable(self) -> bool:
44-
return self.get_type() == 'block_code'
50+
return self.get_type() == self._BLOCK_CODE
51+
52+
def is_expectation(self) -> bool:
53+
return self.get_type() == self._BLOCK_CODE and self._EXPECTATION in self._get_annotations()
54+
55+
def get_expectation_metadata(self) -> dict:
56+
return self.node.get('attrs', {}).get('info', '').split(' ')
57+
58+
def _has_annotation(self, annotation: str) -> List[str]:
59+
return annotation in self._get_annotations()
60+
61+
def has_annotation(self, annotation: str) -> bool:
62+
return self._has_annotation(annotation)
4563

4664
def _get_annotations(self) -> List[str]:
4765
return self.node.get('attrs', {}).get('info', '').split(' ')
@@ -81,6 +99,12 @@ def get_ordered(self) -> List[ASTNode]:
8199

82100
def expand(self, node: ASTNode) -> str:
83101
return node.expand()
102+
103+
def __str__(self):
104+
return json.dumps([node.node for node in self._ast], indent=2)
105+
106+
def __repr__(self):
107+
return self.__str__()
84108

85109

86110

@@ -92,13 +116,58 @@ def parse_markdown_file(self, file_path: str, lang=None) -> MdAST:
92116
txt = f.read()
93117
raw_list: List[dict] = markdown(txt)
94118
return MdAST([ASTNode(node) for node in raw_list])
119+
120+
class Expectation(object):
121+
122+
_STDOUT_TABLE_CONTAINS_DATA: str = 'stdout-table-contains-data'
123+
124+
def __init__(self, node: ASTNode):
125+
self._node: ASTNode = node
126+
127+
def _contains_nonempty_table(self, s: str) -> bool:
128+
required_run_length: int = 5
129+
lines = s.split('\n')
130+
print(f'lines: {lines}')
131+
if len(lines) < required_run_length:
132+
return False
133+
run_length: int = 0
134+
for line in lines:
135+
if line.startswith('|'):
136+
run_length += 1
137+
if run_length == required_run_length:
138+
return True
139+
else:
140+
run_length = 0
141+
return False
142+
143+
def passes_stdout(self, actual_stdout: bytes) -> bool:
144+
actual_stdout_str: str = actual_stdout.decode(sys.getdefaultencoding())
145+
if self._node.has_annotation(self._STDOUT_TABLE_CONTAINS_DATA):
146+
return self._contains_nonempty_table(actual_stdout_str)
147+
return True
148+
149+
def passes_stderr(self, actual_stderr: bytes) -> bool:
150+
return True
151+
152+
def __str__(self):
153+
return str(self._node)
154+
155+
def __repr__(self):
156+
return self.__str__()
95157

96158
class WorkloadDTO(object):
97159

98-
def __init__(self, setup: str, in_session: List[str], teardown: str):
160+
def __init__(
161+
self,
162+
setup: str,
163+
in_session: List[str],
164+
teardown: str,
165+
expectations: List[Expectation]
166+
):
99167
self._setup = setup
100168
self._in_session = in_session
101169
self._teardown = teardown
170+
self._expectations = expectations
102171

103172
def get_setup(self) -> List[str]:
104173
return self._setup
@@ -109,8 +178,11 @@ def get_in_session(self) -> List[str]:
109178
def get_teardown(self) -> List[str]:
110179
return self._teardown
111180

181+
def get_expectations(self) -> List[Expectation]:
182+
return self._expectations
183+
112184
def __str__(self):
113-
return f'Setup: {self._setup}\nIn Session: {self._in_session}\nTeardown: {self._teardown}'
185+
return f'Setup: {self._setup}\nIn Session: {self._in_session}\nTeardown: {self._teardown}\nExpectations: {self._expectations}'
114186

115187
def __repr__(self):
116188
return self.__str__()
@@ -140,8 +212,12 @@ def orchestrate(self, file_path: str) -> WorkloadDTO:
140212
setup_str: str = f'cd {_REPOSITORY_ROOT_PATH};\n'
141213
in_session_commands: List[str] = []
142214
teardown_str: str = f'cd {_REPOSITORY_ROOT_PATH};\n'
215+
expectations: List[Expectation] = []
143216
for node in ast.get_ordered():
144-
if node.is_executable():
217+
if node.is_expectation():
218+
expectations.append(Expectation(node))
219+
continue
220+
elif node.is_executable():
145221
if node.is_setup():
146222
if setup_count < self._max_setup_blocks:
147223
setup_str += ast.expand(node)
@@ -161,7 +237,7 @@ def orchestrate(self, file_path: str) -> WorkloadDTO:
161237
invocation_count += 1
162238
else:
163239
raise KeyError(f'Maximum invocation blocks exceeded: {self._max_invocations_blocks}')
164-
return WorkloadDTO(setup_str, in_session_commands, teardown_str)
240+
return WorkloadDTO(setup_str, in_session_commands, teardown_str, expectations)
165241

166242
class SimpleE2E(object):
167243

@@ -194,6 +270,11 @@ def run(self) -> Tuple[bytes, bytes]:
194270
stdout_bytes, stderr_bytes = e2e.run()
195271
print(stdout_bytes.decode(sys.getdefaultencoding()))
196272
print(stderr_bytes.decode(sys.getdefaultencoding()))
273+
for expectation in workload_dto.get_expectations():
274+
print(f'Expectation: {expectation}')
275+
print(f'Passes stdout: {expectation.passes_stdout(stdout_bytes)}')
276+
print(f'Passes stderr: {expectation.passes_stderr(stderr_bytes)}')
277+
print('---')
197278

198279

199280

0 commit comments

Comments
 (0)