Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
111 changes: 111 additions & 0 deletions ocp-build-data-validator/tests/test_streams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import unittest

from validator import streams


class TestStreams(unittest.TestCase):
def test_no_aliases_no_collision(self):
data = {
"golang": {
"image": "openshift/golang-builder:v1.0",
},
"rhel": {
"image": "openshift/ose-base:ubi8",
},
}
err = streams.validate(data)
self.assertIsNone(err)

def test_aliases_no_collision(self):
data = {
"golang": {
"image": "openshift/golang-builder:v1.0",
"aliases": ["go-toolset"],
},
"rhel": {
"image": "openshift/ose-base:ubi8",
"aliases": ["base-rhel"],
},
}
err = streams.validate(data)
self.assertIsNone(err)

def test_alias_collides_with_stream_name(self):
data = {
"golang": {
"image": "openshift/golang-builder:v1.0",
"aliases": ["rhel"],
},
"rhel": {
"image": "openshift/ose-base:ubi8",
},
}
err = streams.validate(data)
self.assertIsNotNone(err)
self.assertIn("Alias 'rhel' in stream 'golang' collides with a top-level stream name", err)

def test_alias_collides_with_own_stream_name(self):
data = {
"golang": {
"image": "openshift/golang-builder:v1.0",
"aliases": ["golang"],
},
}
err = streams.validate(data)
self.assertIsNotNone(err)
self.assertIn("Alias 'golang' in stream 'golang' collides with a top-level stream name", err)

def test_duplicate_alias_across_streams(self):
data = {
"golang": {
"image": "openshift/golang-builder:v1.0",
"aliases": ["go-toolset"],
},
"golang-alt": {
"image": "openshift/golang-builder:v2.0",
"aliases": ["go-toolset"],
},
}
err = streams.validate(data)
self.assertIsNotNone(err)
self.assertIn("Alias 'go-toolset' is defined in both stream 'golang' and stream 'golang-alt'", err)

def test_multiple_collisions_reported(self):
data = {
"golang": {
"image": "openshift/golang-builder:v1.0",
"aliases": ["rhel", "shared-alias"],
},
"rhel": {
"image": "openshift/ose-base:ubi8",
"aliases": ["shared-alias"],
},
}
err = streams.validate(data)
self.assertIsNotNone(err)
self.assertIn("Alias 'rhel' in stream 'golang' collides with a top-level stream name", err)
self.assertIn("Alias 'shared-alias' is defined in both stream 'golang' and stream 'rhel'", err)

def test_duplicate_alias_within_same_stream(self):
"""Duplicate aliases within the same stream should not be flagged as cross-stream collisions."""
data = {
"golang": {
"image": "openshift/golang-builder:v1.0",
"aliases": ["go-toolset", "go-toolset"],
},
}
err = streams.validate(data)
self.assertIsNone(err)

def test_empty_aliases_list_no_collision(self):
data = {
"golang": {
"image": "openshift/golang-builder:v1.0",
"aliases": [],
},
"rhel": {
"image": "openshift/ose-base:ubi8",
},
}
err = streams.validate(data)
self.assertIsNone(err)
12 changes: 10 additions & 2 deletions ocp-build-data-validator/validator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import sys
from multiprocessing import Pool, cpu_count

from . import exceptions, format, github, global_session, releases, schema, support
from . import exceptions, format, github, global_session, releases, schema, streams, support


def validate(file, schema_only, images_dir):
Expand All @@ -25,7 +25,7 @@ def validate(file, schema_only, images_dir):
msg = 'Schema mismatch: {}\nReturned error: {}'.format(file, err)
support.fail_validation(msg, parsed)

if support.get_artifact_type(file) not in ['image', 'rpm', 'releases', 'group']:
if support.get_artifact_type(file) not in ['image', 'rpm', 'releases', 'group', 'streams']:
print(f'✅ Validated {file}')
return

Expand All @@ -46,6 +46,14 @@ def validate(file, schema_only, images_dir):
print(f'✅ Validated {file}')
return

if support.get_artifact_type(file) == 'streams':
err = streams.validate(parsed)
if err:
msg = "streams.yml validation failed\nReturned error: {}".format(err)
support.fail_validation(msg, parsed)
print(f'✅ Validated {file}')
return

group_cfg = support.load_group_config_for(file)

(url, err) = github.validate(parsed, group_cfg)
Expand Down
46 changes: 46 additions & 0 deletions ocp-build-data-validator/validator/streams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import Optional


def validate(data: dict) -> Optional[str]:
errors = []

alias_collision_error = get_alias_collisions(data)
if alias_collision_error:
errors.append(alias_collision_error)

if errors:
return "; ".join(errors)


def get_alias_collisions(streams_data: dict) -> Optional[str]:
"""
Check for collisions between top-level stream names and alias values.

A collision occurs when:
- An alias value matches a top-level stream name
- The same alias value appears in multiple streams
"""
stream_names = set(streams_data.keys())
alias_to_stream = {} # Maps each alias value to the stream that defines it
collisions = []

for stream_name, stream_config in streams_data.items():
aliases = stream_config.get("aliases", [])
if not aliases:
continue

for alias in aliases:
# Check if alias collides with a top-level stream name
if alias in stream_names:
collisions.append(f"Alias '{alias}' in stream '{stream_name}' collides with a top-level stream name")

# Check if alias is defined by another stream
if alias in alias_to_stream and alias_to_stream[alias] != stream_name:
collisions.append(
f"Alias '{alias}' is defined in both stream '{alias_to_stream[alias]}' and stream '{stream_name}'"
)
elif alias not in alias_to_stream:
alias_to_stream[alias] = stream_name

if collisions:
return "Stream alias collisions found: " + "; ".join(collisions)
Loading