mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-08-18 12:08:22 +00:00
7a4812c522
* chore: check enterprise-gated files are listed in both license configs A file reachable only through `#[cfg(feature = "enterprise")] mod ...;` is governed by the GreptimeDB Enterprise License, so it must appear in the `includes` of licenserc-enterprise.toml and the `excludes` of licenserc.toml. hawkeye stays silent when it does not: the file keeps its Apache-2.0 header and passes the default check precisely because it was never excluded from it. scripts/check-enterprise-license.py walks enterprise-gated `mod` declarations, resolves them to files (submodules included) and diffs that set against both configs, also reporting stale entries. It runs in the license job in CI and as `make check-enterprise-license`. Documents the split it cannot decide for you — whole enterprise features get their own file, a gated match arm stays inline — in .agents/architecture-invariants.md. Signed-off-by: Dennis Zhuang <killme2008@gmail.com> * fix: tighten enterprise license checks Signed-off-by: Dennis Zhuang <killme2008@gmail.com> --------- Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
227 lines
6.7 KiB
Python
227 lines
6.7 KiB
Python
# Copyright 2023 Greptime Team
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import importlib.util
|
|
import tempfile
|
|
import textwrap
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
SCRIPT_PATH = Path(__file__).resolve().parent / "check-enterprise-license.py"
|
|
|
|
spec = importlib.util.spec_from_file_location("check_enterprise_license", SCRIPT_PATH)
|
|
checker = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(checker)
|
|
|
|
|
|
def write(root: Path, relative: str, source: str) -> Path:
|
|
path = root / relative
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(textwrap.dedent(source).lstrip(), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
class ParseModDeclsTest(unittest.TestCase):
|
|
def parse(self, source: str) -> dict[str, bool]:
|
|
return dict(checker.parse_mod_decls(textwrap.dedent(source)))
|
|
|
|
def test_gate_applies_only_to_the_next_declaration(self):
|
|
self.assertEqual(
|
|
self.parse(
|
|
"""
|
|
#[cfg(feature = "enterprise")]
|
|
pub mod trigger;
|
|
mod plain;
|
|
"""
|
|
),
|
|
{"trigger": True, "plain": False},
|
|
)
|
|
|
|
def test_test_only_gate_counts_as_enterprise(self):
|
|
self.assertEqual(
|
|
self.parse(
|
|
"""
|
|
#[cfg(all(test, feature = "enterprise"))]
|
|
mod recycle_bin_test;
|
|
"""
|
|
),
|
|
{"recycle_bin_test": True},
|
|
)
|
|
|
|
def test_non_gating_enterprise_attributes_are_ignored(self):
|
|
self.assertEqual(
|
|
self.parse(
|
|
"""
|
|
#[cfg(any(test, feature = "enterprise"))]
|
|
mod shared_test;
|
|
#[cfg_attr(feature = "enterprise", allow(dead_code))]
|
|
mod shared;
|
|
"""
|
|
),
|
|
{"shared_test": False, "shared": False},
|
|
)
|
|
|
|
def test_doc_comment_between_attribute_and_declaration(self):
|
|
self.assertEqual(
|
|
self.parse(
|
|
"""
|
|
#[cfg(feature = "enterprise")]
|
|
/// Enterprise only.
|
|
pub(crate) mod gated;
|
|
"""
|
|
),
|
|
{"gated": True},
|
|
)
|
|
|
|
def test_attribute_on_the_same_line(self):
|
|
self.assertEqual(
|
|
self.parse('#[cfg(feature = "enterprise")] mod gated;\n'),
|
|
{"gated": True},
|
|
)
|
|
|
|
def test_attribute_consumed_by_another_item_does_not_leak(self):
|
|
self.assertEqual(
|
|
self.parse(
|
|
"""
|
|
#[cfg(feature = "enterprise")]
|
|
use crate::gated::Thing;
|
|
mod plain;
|
|
"""
|
|
),
|
|
{"plain": False},
|
|
)
|
|
|
|
def test_other_features_are_not_enterprise(self):
|
|
self.assertEqual(
|
|
self.parse(
|
|
"""
|
|
#[cfg(feature = "testing")]
|
|
mod helper;
|
|
"""
|
|
),
|
|
{"helper": False},
|
|
)
|
|
|
|
def test_inline_module_is_ignored(self):
|
|
self.assertEqual(
|
|
self.parse(
|
|
"""
|
|
#[cfg(feature = "enterprise")]
|
|
mod inline {
|
|
pub const A: u8 = 1;
|
|
}
|
|
"""
|
|
),
|
|
{},
|
|
)
|
|
|
|
|
|
class CollectGatedFilesTest(unittest.TestCase):
|
|
def collect(self, root: Path):
|
|
rust_files = sorted(root.rglob("*.rs"))
|
|
return checker.collect_gated_files(rust_files)
|
|
|
|
def test_descendants_of_a_gated_module_are_gated(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
write(
|
|
root,
|
|
"src/lib.rs",
|
|
"""
|
|
#[cfg(feature = "enterprise")]
|
|
pub mod gated;
|
|
pub mod plain;
|
|
""",
|
|
)
|
|
write(root, "src/gated.rs", "pub mod nested;\n")
|
|
write(root, "src/gated/nested.rs", "pub const A: u8 = 1;\n")
|
|
write(root, "src/plain.rs", "pub const B: u8 = 2;\n")
|
|
|
|
gated, unresolved = self.collect(root)
|
|
|
|
self.assertEqual(
|
|
gated,
|
|
{root / "src/gated.rs", root / "src/gated/nested.rs"},
|
|
)
|
|
self.assertEqual(unresolved, [])
|
|
|
|
def test_module_declared_as_directory(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
write(
|
|
root,
|
|
"src/lib.rs",
|
|
"""
|
|
#[cfg(feature = "enterprise")]
|
|
mod gated;
|
|
""",
|
|
)
|
|
write(root, "src/gated/mod.rs", "mod leaf;\n")
|
|
write(root, "src/gated/leaf.rs", "pub const A: u8 = 1;\n")
|
|
|
|
gated, unresolved = self.collect(root)
|
|
|
|
self.assertEqual(
|
|
gated,
|
|
{root / "src/gated/mod.rs", root / "src/gated/leaf.rs"},
|
|
)
|
|
self.assertEqual(unresolved, [])
|
|
|
|
def test_missing_file_is_reported(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
declaring = write(
|
|
root,
|
|
"src/lib.rs",
|
|
"""
|
|
#[cfg(feature = "enterprise")]
|
|
mod elsewhere;
|
|
""",
|
|
)
|
|
|
|
gated, unresolved = self.collect(root)
|
|
|
|
self.assertEqual(gated, set())
|
|
self.assertEqual(unresolved, [(declaring, "elsewhere")])
|
|
|
|
|
|
class ConfigSyncTest(unittest.TestCase):
|
|
def test_load_marked_paths(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
config = write(
|
|
Path(temp_dir),
|
|
"licenserc.toml",
|
|
"""
|
|
excludes = [
|
|
"copied.rs",
|
|
# enterprise:start
|
|
"gated.rs",
|
|
# enterprise:end
|
|
]
|
|
""",
|
|
)
|
|
|
|
self.assertEqual(
|
|
checker.load_marked_paths(
|
|
config,
|
|
checker.ENTERPRISE_EXCLUDES_START,
|
|
checker.ENTERPRISE_EXCLUDES_END,
|
|
),
|
|
{"gated.rs"},
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|