Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Tooling] Add --exclude flag to Generator to support field removal testing #1411

Merged
merged 45 commits into from
May 24, 2021
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
76ab51e
commit with example test files, to be removed at a later date
May 12, 2021
a407d11
Merge branch 'master' into exclude_set
May 12, 2021
2cc6523
tbc changelog message
May 12, 2021
b530f5b
update PR
May 12, 2021
071bb63
fixed regression
May 12, 2021
b3f5bf7
reorder imports
May 12, 2021
c23266c
make comments more consistent
May 12, 2021
1174813
make comments more consistent
May 12, 2021
8c9eb53
refactor add exclude_filter.py
May 13, 2021
7305ea2
simplified console messages
May 13, 2021
853b0c3
simplify console messages
May 13, 2021
b787790
Merge branch 'master' into exclude_set
djptek May 13, 2021
8d91616
separated calls to subsetfulter exclude_filter for clarity
May 13, 2021
14e05a7
added trailing newlines to test files
May 13, 2021
6cdbcd0
removed fake test files
May 13, 2021
fead411
refactor - move shared defs from subset to loader
May 14, 2021
cbd45c3
refactor - move warn to loader
May 14, 2021
30f88b5
moved test_eval_globs to loader unit tests
May 14, 2021
d17e7b1
moved test_eval_globs to loader unit tests
May 14, 2021
20bc1a5
add test_schema_exclude_filter
May 14, 2021
8764112
test_load_exclude_definitions_raises_when_no_exclude_found
May 14, 2021
29b56b6
added test_exclude_field
May 14, 2021
f32b83c
added test_exclude_fields
May 14, 2021
17c62ef
added test_exclude_non_existing_field_set
May 14, 2021
c8c1c70
added test_exclude_non_existing_field
May 14, 2021
90f0b5f
added test_exclude_field_deep_path
May 14, 2021
c5a7a2a
adding test_exclude_non_existing_field_deep_path
May 14, 2021
a196562
WIP - test_exclude_field_deep_path is failing
May 14, 2021
9128f6e
resolved test_exclude_field_deep_path
May 14, 2021
ed1e18b
Merge branch 'master' into exclude_set
djptek May 14, 2021
2dab2af
remove print statement
May 14, 2021
c0a97c7
Merge branch 'master' into exclude_set
May 17, 2021
3e65530
Merge branch 'master' into exclude_set
djptek May 19, 2021
fa1a9e7
Merge branch 'exclude_set' of https:/djptek/ecs into excl…
May 19, 2021
e52c94f
Merge branch 'master' into exclude_set
May 20, 2021
2e5abb0
normalize use of 3xdouble quote in comments
May 20, 2021
8488069
removed exclude-set.yml
May 20, 2021
3972bea
update USAGE and add dot paths
May 20, 2021
6782f67
add test_exclude_field_dot_path test_exclude_field_base_always_persists
May 20, 2021
a17bc29
update tests to reflect delete vestigial parent
May 20, 2021
c1a57c6
fix #1426 thanks @ebeahan
May 20, 2021
da24cc5
fix comment
May 20, 2021
847a0bc
remove unused imports
May 20, 2021
b66d63e
remove unused imports part 2
May 20, 2021
f1750a7
removed unused imports the gift that just keeps giving
May 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.next.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Thanks, you're awesome :-) -->
#### Improvements

* Fix ecs GitHub repo link source branch #1393
* Add --exclude flag to Generator to support field removal testing #1411

#### Deprecated

Expand Down
5 changes: 5 additions & 0 deletions rfcs/text/0017/exclude-set.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
djptek marked this conversation as resolved.
Show resolved Hide resolved
djptek marked this conversation as resolved.
Show resolved Hide resolved
- name: log
fields:
- name: original

4 changes: 4 additions & 0 deletions scripts/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from schema import cleaner
from schema import finalizer
from schema import subset_filter
from schema import exclude_filter


def main():
Expand Down Expand Up @@ -48,6 +49,7 @@ def main():
cleaner.clean(fields, strict=args.strict)
finalizer.finalize(fields)
fields = subset_filter.filter(fields, args.subset, out_dir)
fields = exclude_filter.exclude(fields, args.exclude, out_dir)
nested, flat = intermediate_files.generate(fields, os.path.join(out_dir, 'ecs'), default_dirs)

if args.intermediate_only:
Expand All @@ -70,6 +72,8 @@ def argument_parser():
Note that "--include experimental/schemas" will also respect this git ref.')
parser.add_argument('--include', nargs='+',
help='include user specified directory of custom field definitions')
parser.add_argument('--exclude', nargs='+',
djptek marked this conversation as resolved.
Show resolved Hide resolved
help='exclude user specified subset of the schema')
parser.add_argument('--subset', nargs='+',
help='render a subset of the schema')
parser.add_argument('--out', action='store', help='directory to output the generated files')
Expand Down
62 changes: 62 additions & 0 deletions scripts/schema/exclude_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import glob
djptek marked this conversation as resolved.
Show resolved Hide resolved
import yaml
import os
from schema import loader

# This script should be run downstream of the subset filters - it takes
# all ECS and custom fields already loaded by the latter and explicitly
# removes a subset, for example, to simulate impact of future removals


def exclude(fields, exclude_file_globs, out_dir):
excludes = load_exclude_definitions(exclude_file_globs)

if excludes:
fields = exclude_fields(fields, excludes)

return fields


def pop_field(fields, node_path, path):
"""pops a field from yaml derived dict using path derived from ordered list of nodes"""
if node_path[0] in fields:
if len(node_path) == 1:
fields.pop(node_path[0])
else:
inner_field = node_path.pop(0)
if 'fields' in fields[inner_field]:
pop_field(fields[inner_field]['fields'], node_path, path)
else:
raise ValueError(
'--exclude specified, but no path to field {} found'.format('.'.join([e for e in path])))
else:
raise ValueError('--exclude specified, but no field {} found'.format('.'.join([e for e in path])))


def exclude_trace_path(fields, item, path):
"""traverses paths to one or more nodes in a yaml derived dict"""
for list_item in item:
node_path = path.copy()
node_path.append(list_item['name'])
if not 'fields' in list_item:
pop_field(fields, node_path, node_path.copy())
else:
exclude_trace_path(fields, list_item['fields'], node_path)


def exclude_fields(fields, excludes):
"""Traverses fields and eliminates any field which matches the excludes"""
if excludes:
for ex_list in excludes:
for item in ex_list:
exclude_trace_path(fields, item['fields'], [item['name']])
return fields


def load_exclude_definitions(file_globs):
if not file_globs:
return []
excludes = loader.load_definitions(file_globs)
if not excludes:
raise ValueError('--exclude specified, but no exclusions found in {}'.format(file_globs))
return excludes
30 changes: 30 additions & 0 deletions scripts/schema/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,33 @@ def merge_fields(a, b):
a[key].setdefault('fields', {})
a[key]['fields'] = merge_fields(a[key]['fields'], b[key]['fields'])
return a


def load_yaml_file(file_name):
with open(file_name) as f:
return yaml.safe_load(f.read())


# You know, for silent tests
def warn(message):
print(message)


def eval_globs(globs):
'''Accepts an array of glob patterns or file names, returns the array of actual files'''
djptek marked this conversation as resolved.
Show resolved Hide resolved
all_files = []
for g in globs:
new_files = glob.glob(g)
if len(new_files) == 0:
warn("{} did not match any files".format(g))
else:
all_files.extend(new_files)
return all_files


def load_definitions(file_globs):
sets = []
for f in eval_globs(file_globs):
raw = load_yaml_file(f)
sets.append(raw)
return sets
29 changes: 2 additions & 27 deletions scripts/schema/subset_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import yaml
import os
from generators import intermediate_files
from schema import cleaner
from schema import cleaner, loader

# This script takes all ECS and custom fields already loaded, and lets users
# filter out the ones they don't need.
Expand Down Expand Up @@ -33,37 +33,12 @@ def combine_all_subsets(subsets):
def load_subset_definitions(file_globs):
if not file_globs:
return []
subsets = []
for f in eval_globs(file_globs):
raw = load_yaml_file(f)
subsets.append(raw)
subsets = loader.load_definitions(file_globs)
if not subsets:
raise ValueError('--subset specified, but no subsets found in {}'.format(file_globs))
return subsets


def load_yaml_file(file_name):
with open(file_name) as f:
return yaml.safe_load(f.read())


def eval_globs(globs):
'''Accepts an array of glob patterns or file names, returns the array of actual files'''
all_files = []
for g in globs:
new_files = glob.glob(g)
if len(new_files) == 0:
warn("{} did not match any files".format(g))
else:
all_files.extend(new_files)
return all_files


# You know, for silent tests
def warn(message):
print(message)


ecs_options = ['fields', 'enabled', 'index']


Expand Down
97 changes: 97 additions & 0 deletions scripts/tests/unit/test_schema_exclude_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from unittest import result
from unittest.case import TestCase
from schema import exclude_filter
import mock
import os
import pprint
djptek marked this conversation as resolved.
Show resolved Hide resolved
import sys
import unittest

sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))


class TestSchemaExcludeFilter(unittest.TestCase):

def setUp(self):
self.maxDiff = None

@mock.patch('schema.loader.warn')
def test_load_exclude_definitions_raises_when_no_exclude_found(self, mock_warn):
with self.assertRaisesRegex(ValueError,
"--exclude specified, but no exclusions found in \['foo\*.yml'\]"):
exclude_filter.load_exclude_definitions(['foo*.yml'])

def test_exclude_field(self):
fields = {'my_field_set': {'fields': {
'my_field_exclude': {'field_details': {'flat_name': 'my_field_set.my_field_exclude'}},
'my_field_persist': {'field_details': {'flat_name': 'my_field_set.my_field_persist'}}}}}
excludes = [
[{'name': 'my_field_set', 'fields': [{'name': 'my_field_exclude'}]}]]
fields = exclude_filter.exclude_fields(fields, excludes)
expect_persisted = {'my_field_set': {'fields': {
'my_field_persist': {'field_details': {'flat_name': 'my_field_set.my_field_persist'}}}}}
self.assertEqual(fields, expect_persisted)

def test_exclude_field_deep_path(self):
fields = {'d0': {'fields': {
'd1': {'field_details': {'flat_name': 'd0.d1'}, 'fields': {
'd2': {'field_details': {'flat_name': 'd0.d1.d2'}, 'fields': {
'd3': {'field_details': {'flat_name': 'd0.d1.d2.d3'}, 'fields': {
'd4': {'field_details': {'flat_name': 'd0.d1.d2.d3.d4'}, 'fields': {
'd5': {'field_details': {'flat_name': 'd0.d1.d2.d3.d4.d5'}}}}}}}}}}}}}
excludes = [[{'name': 'd0', 'fields': [{
'name': 'd1', 'fields': [{
'name': 'd2', 'fields': [{
'name': 'd3', 'fields': [{
'name': 'd4', 'fields': [{
'name': 'd5'}]}]}]}]}]}]]
fields = exclude_filter.exclude_fields(fields, excludes)
expect_persisted = {'d0': {'fields': {
'd1': {'field_details': {'flat_name': 'd0.d1'}, 'fields': {
'd2': {'field_details': {'flat_name': 'd0.d1.d2'}, 'fields': {
'd3': {'field_details': {'flat_name': 'd0.d1.d2.d3'}, 'fields': {
'd4': {'field_details': {'flat_name': 'd0.d1.d2.d3.d4'}, 'fields': {}}}}}}}}}}}
self.assertEqual(fields, expect_persisted)

def test_exclude_fields(self):
fields = {'my_field_set': {'fields': {
'my_field_exclude_1': {'field_details': {'flat_name': 'my_field_set.my_field_exclude_1'}},
'my_field_exclude_2': {'field_details': {'flat_name': 'my_field_set.my_field_exclude_2'}}}}}
excludes = [[{'name': 'my_field_set', 'fields': [
{'name': 'my_field_exclude_1'}, {'name': 'my_field_exclude_2'}]}]]
fields = exclude_filter.exclude_fields(fields, excludes)
expect_persisted = {'my_field_set': {'fields': {}}}
self.assertEqual(fields, expect_persisted)

def test_exclude_non_existing_field_set(self):
fields = {'my_field_set': {'fields': {
'my_field': {'field_details': {'flat_name': 'my_field_set.my_field'}}}}}
excludes = [[{'name': 'my_non_existing_field_set', 'fields': [
{'name': 'my_field_exclude'}]}]]
with self.assertRaisesRegex(ValueError,
"--exclude specified, but no field my_non_existing_field_set.my_field_exclude found"):
exclude_filter.exclude_fields(fields, excludes)

def test_exclude_non_existing_field(self):
fields = {'my_field_set': {'fields': {
'my_field': {'field_details': {'flat_name': 'my_field_set.my_field'}}}}}
excludes = [[{'name': 'my_field_set', 'fields': [
{'name': 'my_non_existing_field'}]}]]
with self.assertRaisesRegex(ValueError,
"--exclude specified, but no field my_field_set.my_non_existing_field found"):
exclude_filter.exclude_fields(fields, excludes)

def test_exclude_non_existing_field_deep_path(self):
fields = {'d0': {'fields': {
'd1': {'field_details': {'flat_name': 'd0.d1'}}, 'fields': {
'd2': {'field_details': {'flat_name': 'd0.d1.d2'}}, 'fields': {
'd3': {'field_details': {'flat_name': 'd0.d1.d2.d3'}}}}}}}
excludes = [[{'name': 'd0', 'fields': [{
'name': 'd1', 'fields': [{
'name': 'd2', 'fields': [{
'name': 'd3', 'fields': [{
'name': 'd4', 'fields': [{
'name': 'd5'}]}]}]}]}]}]]
with self.assertRaisesRegex(ValueError,
"--exclude specified, but no path to field d0.d1.d2.d3.d4.d5 found"):
exclude_filter.exclude_fields(fields, excludes)
8 changes: 8 additions & 0 deletions scripts/tests/unit/test_schema_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ class TestSchemaLoader(unittest.TestCase):
def setUp(self):
self.maxDiff = None

@mock.patch('schema.loader.warn')
def test_eval_globs(self, mock_warn):
files = loader.eval_globs(['schemas/*.yml', 'missing*'])
self.assertTrue(mock_warn.called, "a warning should have been printed for missing*")
self.assertIn('schemas/base.yml', files)
self.assertEqual(list(filter(lambda f: f.startswith('missing'), files)), [],
"The 'missing*' pattern should not show up in the resulting files")

# Pseudo-fixtures

def schema_base(self):
Expand Down
10 changes: 1 addition & 9 deletions scripts/tests/unit/test_schema_subset_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,7 @@ class TestSchemaSubsetFilter(unittest.TestCase):
def setUp(self):
self.maxDiff = None

@mock.patch('schema.subset_filter.warn')
def test_eval_globs(self, mock_warn):
files = subset_filter.eval_globs(['schemas/*.yml', 'missing*'])
self.assertTrue(mock_warn.called, "a warning should have been printed for missing*")
self.assertIn('schemas/base.yml', files)
self.assertEqual(list(filter(lambda f: f.startswith('missing'), files)), [],
"The 'missing*' pattern should not show up in the resulting files")

@mock.patch('schema.subset_filter.warn')
@mock.patch('schema.loader.warn')
def test_load_subset_definitions_raises_when_no_subset_found(self, mock_warn):
with self.assertRaisesRegex(ValueError,
"--subset specified, but no subsets found in \['foo\*.yml'\]"):
Expand Down