Skip to content

Commit 21ef167

Browse files
Add e2e tests (#6)
* Add e2e tests
1 parent 0dbd98c commit 21ef167

File tree

22 files changed

+356
-9
lines changed

22 files changed

+356
-9
lines changed

.github/workflows/run-tests.yml

+24
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,27 @@ jobs:
3636
run: |
3737
cd $GITHUB_WORKSPACE/test
3838
python -m unittest discover
39+
40+
# Setup config required for Python CLI run
41+
- name: Config setup
42+
run: |
43+
mkdir ~/.rai
44+
cd ~/.rai
45+
echo "[default]" > config
46+
echo "region = us-east" >> config
47+
echo "host = azure.relationalai.com" >> config
48+
echo "port = 443" >> config
49+
echo "client_id = ${{ env.RAI_CLIENT_ID }}" >> config
50+
echo "client_secret = ${{ env.RAI_CLIENT_SECRET }}" >> config
51+
# Create empty toml
52+
cd $GITHUB_WORKSPACE/cli-e2e-test/config
53+
touch loader.toml
54+
env:
55+
RAI_CLIENT_ID: ${{ secrets.client_id }}
56+
RAI_CLIENT_SECRET: ${{ secrets.client_secret }}
57+
58+
# Run e2e Python CLI test
59+
- name: Python CLI tests
60+
run: |
61+
cd $GITHUB_WORKSPACE/cli-e2e-test
62+
python -m unittest discover

.gitignore

-3
Original file line numberDiff line numberDiff line change
@@ -235,9 +235,6 @@ tramp
235235
# elpa packages
236236
/elpa/
237237

238-
# reftex files
239-
*.rel
240-
241238
# AUCTeX auto folder
242239
/auto/
243240

cli-e2e-test/__init__.py

Whitespace-only changes.
+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"workflow": [
3+
{
4+
"type": "ConfigureSources",
5+
"name": "ConfigureSources",
6+
"configFiles": [
7+
"config/scenario1.rel"
8+
],
9+
"sources": [
10+
{
11+
"relation": "zip_city_state_master_data",
12+
"isPartitioned": false,
13+
"relativePath": "master_source/zip_city_state",
14+
"inputFormat": "csv",
15+
"isMaster": true
16+
},
17+
{
18+
"relation": "device_data",
19+
"isPartitioned": true,
20+
"relativePath": "device",
21+
"inputFormat": "csv",
22+
"loadsNumberOfDays": 60
23+
},
24+
{
25+
"relation": "store_data",
26+
"isPartitioned": true,
27+
"relativePath": "store",
28+
"inputFormat": "jsonl",
29+
"extensions": [
30+
"json",
31+
"jsonl"
32+
],
33+
"loadsNumberOfDays": 60
34+
}
35+
]
36+
},
37+
{
38+
"type": "InstallModels",
39+
"name": "InstallModels1",
40+
"modelFiles": [
41+
"device.rel",
42+
"zip.rel",
43+
"store.rel",
44+
"json_schema_mapping.rel"
45+
]
46+
},
47+
{
48+
"type": "LoadData",
49+
"name": "LoadData"
50+
},
51+
{
52+
"type": "Materialize",
53+
"name": "Materialize",
54+
"relations": [
55+
"city:name",
56+
"device:name",
57+
"store:name"
58+
],
59+
"materializeJointly": true
60+
},
61+
{
62+
"type": "Export",
63+
"name": "Export",
64+
"exportJointly": false,
65+
"dateFormat": "%Y%m%d",
66+
"exports": [
67+
{
68+
"type": "csv",
69+
"configRelName": "cities_csv",
70+
"relativePath": "cities"
71+
},
72+
{
73+
"type": "csv",
74+
"configRelName": "devices_csv",
75+
"relativePath": "devices"
76+
},
77+
{
78+
"type": "csv",
79+
"configRelName": "stores_csv",
80+
"relativePath": "stores"
81+
}
82+
]
83+
}
84+
]
85+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
device,IMEI
2+
IPhone1,123
3+
IPhone2,124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ZIP_CODE,CITY_NAME,STATE_CODE,STATE_NAME
2+
90005,Los Angeles,CA,California
3+
90401,Santa Monica,CA,California
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{ "storeId": "0ffc-0909", "storeName": "Store1"}
2+
{ "storeId": "0567-aa98", "storeName": "Store2"}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name
2+
Los Angeles
3+
Santa Monica
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name
2+
IPhone1
3+
IPhone2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name
2+
Store1
3+
Store2

cli-e2e-test/main.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import cli.runner
2+
3+
if __name__ == "__main__":
4+
cli.runner.start()

cli-e2e-test/output/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This dir is used in dev mode to store the output csv of the test data.

cli-e2e-test/rel/config/scenario1.rel

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
@inline
2+
module export_config
3+
module cities_csv
4+
def data = city
5+
6+
def syntax:header = {
7+
1, :name
8+
}
9+
end
10+
11+
module devices_csv
12+
def data = device
13+
14+
def syntax:header = {
15+
1, :name
16+
}
17+
end
18+
19+
module stores_csv
20+
def data = store
21+
22+
def syntax:header = {
23+
1, :name
24+
}
25+
end
26+
end
27+
28+
def part_resource_date_pattern = "^(.+)/data_dt=(?<date>[0-9]+)/(.+).(csv|json|jsonl)$"
29+
def part_resource_index_pattern = "^(.+)/data_dt=(.+)/part-(?<shard>[0-9]).(csv|json|jsonl)$"
30+
def part_resource_index_multiplier = 100000

cli-e2e-test/rel/device.rel

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
bound import_config:device_data:schema
2+
bound syntax:header
3+
4+
module device_data
5+
def DEVICE_NAME[idx, row] = source_catalog:device_data[idx, :device, row]
6+
end
7+
8+
module device
9+
def name = device_data:DEVICE_NAME[_, _]
10+
end
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* The modules in this file automate the schema mapping of JSON or JSONL sources
3+
* that are configured as part of the json_source_config module.
4+
*
5+
* To use this framework, simply populate the json_source_config relation for
6+
* some *source* with the string values of keys for any one or more of the
7+
* following types: (string, int, decimal, and object).
8+
*/
9+
10+
def JsonSourceConfigKey(k) { json_source_config(k, x...) from x... }
11+
12+
def json_source:value[src in JsonSourceConfigKey] = source_catalog[src, _, :value]
13+
def json_source:child[src in JsonSourceConfigKey] = source_catalog[src, _, :child]
14+
def json_source:root[src in JsonSourceConfigKey] = source_catalog[src, _, :root]
15+
def json_source:array[src in JsonSourceConfigKey] = source_catalog[src, _, :array]
16+
17+
def json_source:value[src in JsonSourceConfigKey] = simple_source_catalog[src, :value]
18+
def json_source:child[src in JsonSourceConfigKey] = simple_source_catalog[src, :child]
19+
def json_source:root[src in JsonSourceConfigKey] = simple_source_catalog[src, :root]
20+
def json_source:array[src in JsonSourceConfigKey] = simple_source_catalog[src, :array]
21+
22+
/**
23+
* [REKS] Note that json_source, json_hash, json_name_sym, and json_schema cannot
24+
* be further consolidated into a container module because of limits on the
25+
* use of metafication (i.e., the #(...) operator) inside recursive
26+
* definitions.
27+
*/
28+
def json_hash(src, t, h, s) =
29+
murmurhash3f(s, x) and
30+
h = uint128_hash_value_convert[x] and
31+
json_source_config(src, t, s)
32+
from x
33+
34+
def json_name_sym[src, x] = #(json_hash[src, _, x])
35+
36+
module json_schema
37+
38+
def parent(src, y, x, z) { json_source:child(src, x, y, z) }
39+
40+
def value(src, r, o, val) {
41+
parent(src, r, o, t) and
42+
json_hash(src, :string, r, _) and
43+
json_source:value(src, t, val) and
44+
String(val)
45+
from t
46+
}
47+
48+
def value(src, r, o, v) {
49+
parent(src, r, o, t) and
50+
json_hash(src, :decimal, r, _) and
51+
json_source:value(src, t, v) and
52+
Float(v)
53+
from t
54+
}
55+
56+
def value(src, r, o, v) {
57+
parent(src, r, o, t) and
58+
json_hash(src, :int, r, _) and
59+
json_source:value(src, t, v) and
60+
Int(v)
61+
from t
62+
}
63+
64+
end

cli-e2e-test/rel/store.rel

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
def json_source_config:store_data:string = {
2+
"storeId" ;
3+
"storeName"
4+
}
5+
6+
def json_source_config:store_data:object(k) {
7+
store_data_metadata:object_or_value_key(k) and
8+
not json_source_config:store_data:string(k)
9+
}
10+
11+
module store_data_metadata
12+
13+
def object_or_value_key = covers[_]
14+
15+
def covers = {
16+
"_root_", "storeId" ;
17+
"_root_", "storeName"
18+
}
19+
20+
end
21+
22+
def store_data_value(t, o, v) =
23+
json_schema:value:store_data(h, o, v) and
24+
json_name_sym:store_data(h, t)
25+
from h
26+
27+
def store_data_object(t, o, v) =
28+
json_schema:parent:store_data(h, o, v) and
29+
json_name_sym:store_data(h, t)
30+
from h
31+
32+
module store
33+
def name = store_data_value:storeName[_]
34+
end

cli-e2e-test/rel/zip.rel

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module zip_city_state_master_data
2+
def CITY_NAME[row] = simple_source_catalog:zip_city_state_master_data:CITY_NAME[row]
3+
end
4+
5+
module city
6+
def name = zip_city_state_master_data:CITY_NAME[_]
7+
end

cli-e2e-test/test_e2e.py

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import unittest
2+
import os
3+
import logging
4+
import uuid
5+
6+
import workflow.manager
7+
from csv_diff import load_csv, compare, human_text
8+
from subprocess import call
9+
10+
11+
class CliE2ETest(unittest.TestCase):
12+
logger: logging.Logger
13+
resource_manager: workflow.manager.ResourceManager
14+
output = "./output"
15+
env_config = "./config/loader.toml"
16+
expected = "./expected_results"
17+
resource_name = "wm-cli-e2e-test-" + str(uuid.uuid4())
18+
cmd_with_common_arguments = ["python", "main.py",
19+
"--run-mode", "local",
20+
"--env-config", env_config,
21+
"--engine", resource_name,
22+
"--database", resource_name,
23+
"--rel-config-dir", "./rel",
24+
"--dev-data-dir", "./data",
25+
"--output-root", output]
26+
27+
def test_scamp_aggregates_model(self):
28+
# when
29+
test_args = ["--batch-config", "./config/model/scenario1.json",
30+
"--start-date", "20220103",
31+
"--end-date", "20220105"]
32+
command = self.cmd_with_common_arguments
33+
command += test_args
34+
rsp = call(command)
35+
# then
36+
self.assertNotEqual(rsp, 1)
37+
self.assert_output_dir_files()
38+
39+
@classmethod
40+
def setUpClass(cls) -> None:
41+
# Make sure output folder is empty since the folder share across repository. Remove README.md, other files left.
42+
cleanup_output(cls.output)
43+
cls.logger = logging.getLogger("cli-e2e-test")
44+
cls.resource_manager = workflow.manager.ResourceManager.init(cls.logger, cls.resource_name, cls.resource_name)
45+
cls.logger.setLevel(logging.INFO)
46+
cls.logger.addHandler(logging.StreamHandler())
47+
cls.resource_manager.add_engine()
48+
49+
def tearDown(self):
50+
cleanup_output(self.output)
51+
52+
@classmethod
53+
def tearDownClass(cls) -> None:
54+
cls.resource_manager.cleanup_resources()
55+
56+
def assert_output_dir_files(self):
57+
for filename in os.listdir(self.output):
58+
actual_path = os.path.join(self.output, filename)
59+
expected_path = os.path.join( self.expected, filename)
60+
with open(actual_path, 'r') as actual, open(expected_path, 'r') as expected:
61+
diff = compare(
62+
load_csv(actual),
63+
load_csv(expected)
64+
)
65+
self.logger.info(f"Assert file `{filename}`")
66+
self.assertEqual(human_text(diff), '')
67+
68+
69+
def cleanup_output(directory: str):
70+
for filename in os.listdir(directory):
71+
file_path = os.path.join(directory, filename)
72+
if os.path.isfile(file_path):
73+
os.remove(file_path)

rel/source_configs/config.rel

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ module part_resource
7676
uri:identifies(u, r) and
7777
d = parse_int[ uri:parse[u, "date"] ] and
7878
s = parse_int[ uri:parse[u, shard_alias] ] and
79-
n = ^PartIndex[ d * part_resource_index_multiplier + s ]
79+
n = ^PartIndex[ d * part_resource_index_multiplier + s ]
8080
from u, d, s
8181
}
8282
end

0 commit comments

Comments
 (0)