Generate build date at invocation time
This commit is contained in:
Родитель
c52e0e71a4
Коммит
9dd0bdde69
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -4,6 +4,16 @@
|
|||
|
||||
- Support global file-level tags in metrics.yaml ([bug 1745283](https://bugzilla.mozilla.org/show_bug.cgi?id=1745283))
|
||||
- Glinter: Reject metric files if they use `unit` by mistake. It should be `time_unit` ([#432](https://github.com/mozilla/glean_parser/pull/432)).
|
||||
- Automatically generate a build date when generating build info ([#431](https://github.com/mozilla/glean_parser/pull/431)).
|
||||
Enabled for Kotlin and Swift.
|
||||
This can be changed with the `build_date` command line option.
|
||||
`build_date=0` will use a static unix epoch time.
|
||||
`build_date=2022-01-03T17:30:00` will parse the ISO8601 string to use (as a UTC timestamp).
|
||||
Other values will throw an error.
|
||||
|
||||
Example:
|
||||
|
||||
glean_parser translate --format kotlin --option build_date=2021-11-01T01:00:00 path/to/metrics.yaml
|
||||
|
||||
## 4.3.1
|
||||
|
||||
|
|
|
@ -132,6 +132,29 @@ def class_name(obj_type: str) -> str:
|
|||
return util.Camelize(obj_type) + "MetricType"
|
||||
|
||||
|
||||
def generate_build_date(date: Optional[str]) -> str:
|
||||
"""
|
||||
Generate the build timestamp.
|
||||
"""
|
||||
|
||||
ts = util.build_date(date)
|
||||
|
||||
data = [
|
||||
str(ts.year),
|
||||
# In Java the first month of the year in calendars is JANUARY which is 0.
|
||||
# In Python it's 1-based
|
||||
str(ts.month - 1),
|
||||
str(ts.day),
|
||||
str(ts.hour),
|
||||
str(ts.minute),
|
||||
str(ts.second),
|
||||
]
|
||||
components = ", ".join(data)
|
||||
|
||||
# DatetimeMetricType takes a `Calendar` instance.
|
||||
return f'Calendar.getInstance(TimeZone.getTimeZone("GMT+0")).also {{ cal -> cal.set({components}) }}' # noqa
|
||||
|
||||
|
||||
def output_gecko_lookup(
|
||||
objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
|
||||
) -> None:
|
||||
|
@ -249,6 +272,11 @@ def output_kotlin(
|
|||
- `with_buildinfo`: If "true" a `GleanBuildInfo.kt` file is generated.
|
||||
Otherwise generation of that file is skipped.
|
||||
Defaults to "true".
|
||||
- `build_date`: If set to `0` a static unix epoch time will be used.
|
||||
If set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
|
||||
it will use that date.
|
||||
Other values will throw an error.
|
||||
If not set it will use the current date & time.
|
||||
"""
|
||||
if options is None:
|
||||
options = {}
|
||||
|
@ -257,6 +285,7 @@ def output_kotlin(
|
|||
glean_namespace = options.get("glean_namespace", "mozilla.components.service.glean")
|
||||
namespace_package = namespace[: namespace.rfind(".")]
|
||||
with_buildinfo = options.get("with_buildinfo", "true").lower() == "true"
|
||||
build_date = options.get("build_date", None)
|
||||
|
||||
# Write out the special "build info" object
|
||||
template = util.get_jinja2_template(
|
||||
|
@ -264,6 +293,7 @@ def output_kotlin(
|
|||
)
|
||||
|
||||
if with_buildinfo:
|
||||
build_date = generate_build_date(build_date)
|
||||
# This filename needs to start with "Glean" so it can never clash with a
|
||||
# metric category
|
||||
with (output_dir / "GleanBuildInfo.kt").open("w", encoding="utf-8") as fd:
|
||||
|
@ -272,6 +302,7 @@ def output_kotlin(
|
|||
namespace=namespace,
|
||||
namespace_package=namespace_package,
|
||||
glean_namespace=glean_namespace,
|
||||
build_date=build_date,
|
||||
)
|
||||
)
|
||||
fd.write("\n")
|
||||
|
|
|
@ -134,6 +134,33 @@ def variable_name(var: str) -> str:
|
|||
return var
|
||||
|
||||
|
||||
class BuildInfo:
|
||||
def __init__(self, build_date):
|
||||
self.build_date = build_date
|
||||
|
||||
|
||||
def generate_build_date(date: Optional[str]) -> str:
|
||||
"""
|
||||
Generate the build timestamp.
|
||||
"""
|
||||
|
||||
ts = util.build_date(date)
|
||||
|
||||
data = [
|
||||
("year", ts.year),
|
||||
("month", ts.month),
|
||||
("day", ts.day),
|
||||
("hour", ts.hour),
|
||||
("minute", ts.minute),
|
||||
("second", ts.second),
|
||||
]
|
||||
|
||||
# The internal DatetimeMetricType API can take a `DateComponents` object,
|
||||
# which lets us easily specify the timezone.
|
||||
components = ", ".join([f"{name}: {val}" for (name, val) in data])
|
||||
return f'DateComponents(calendar: Calendar.current, timeZone: TimeZone(abbreviation: "UTC"), {components})' # noqa
|
||||
|
||||
|
||||
class Category:
|
||||
"""
|
||||
Data struct holding information about a metric to be used in the template.
|
||||
|
@ -157,6 +184,14 @@ def output_swift(
|
|||
- namespace: The namespace to generate metrics in
|
||||
- glean_namespace: The namespace to import Glean from
|
||||
- allow_reserved: When True, this is a Glean-internal build
|
||||
- with_buildinfo: If "true" the `GleanBuildInfo` is generated.
|
||||
Otherwise generation of that file is skipped.
|
||||
Defaults to "true".
|
||||
- build_date: If set to `0` a static unix epoch time will be used.
|
||||
If set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
|
||||
it will use that date.
|
||||
Other values will throw an error.
|
||||
If not set it will use the current date & time.
|
||||
"""
|
||||
if options is None:
|
||||
options = {}
|
||||
|
@ -174,6 +209,12 @@ def output_swift(
|
|||
|
||||
namespace = options.get("namespace", "GleanMetrics")
|
||||
glean_namespace = options.get("glean_namespace", "Glean")
|
||||
with_buildinfo = options.get("with_buildinfo", "true").lower() == "true"
|
||||
build_date = options.get("build_date", None)
|
||||
build_info = None
|
||||
if with_buildinfo:
|
||||
build_date = generate_build_date(build_date)
|
||||
build_info = BuildInfo(build_date=build_date)
|
||||
|
||||
filename = "Metrics.swift"
|
||||
filepath = output_dir / filename
|
||||
|
@ -199,6 +240,7 @@ def output_swift(
|
|||
namespace=namespace,
|
||||
glean_namespace=glean_namespace,
|
||||
allow_reserved=options.get("allow_reserved", False),
|
||||
build_info=build_info,
|
||||
)
|
||||
)
|
||||
# Jinja2 squashes the final newline, so we explicitly add it
|
||||
|
|
|
@ -14,14 +14,18 @@ Jinja2 template is not. Please file bugs! #}
|
|||
|
||||
package {{ namespace }}
|
||||
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
import {{ glean_namespace }}.BuildInfo
|
||||
import {{ namespace_package }}.BuildConfig
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal object GleanBuildInfo {
|
||||
val buildInfo: BuildInfo by lazy {
|
||||
BuildInfo(
|
||||
versionCode = BuildConfig.VERSION_CODE.toString(),
|
||||
versionName = BuildConfig.VERSION_NAME
|
||||
versionName = BuildConfig.VERSION_NAME,
|
||||
buildDate = {{ build_date }}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,16 @@ import {{ glean_namespace }}
|
|||
// swiftlint:disable force_try
|
||||
|
||||
extension {{ namespace }} {
|
||||
{% if build_info %}
|
||||
class GleanBuild {
|
||||
private init() {
|
||||
// Intentionally left private, no external user can instantiate a new global object.
|
||||
}
|
||||
|
||||
public static let info = BuildInfo(buildDate: {{ build_info.build_date }})
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% for category in categories %}
|
||||
{% if category.contains_pings %}
|
||||
class {{ category.name|Camelize }} {
|
||||
|
|
|
@ -24,6 +24,26 @@ import yaml
|
|||
if sys.version_info < (3, 7):
|
||||
import iso8601 # type: ignore
|
||||
|
||||
def date_fromisoformat(datestr: str) -> datetime.date:
|
||||
try:
|
||||
return iso8601.parse_date(datestr).date()
|
||||
except iso8601.ParseError:
|
||||
raise ValueError()
|
||||
|
||||
def datetime_fromisoformat(datestr: str) -> datetime.datetime:
|
||||
try:
|
||||
return iso8601.parse_date(datestr)
|
||||
except iso8601.ParseError:
|
||||
raise ValueError()
|
||||
|
||||
else:
|
||||
|
||||
def date_fromisoformat(datestr: str) -> datetime.date:
|
||||
return datetime.date.fromisoformat(datestr)
|
||||
|
||||
def datetime_fromisoformat(datestr: str) -> datetime.datetime:
|
||||
return datetime.datetime.fromisoformat(datestr)
|
||||
|
||||
|
||||
TESTING_MODE = "pytest" in sys.modules
|
||||
|
||||
|
@ -360,13 +380,7 @@ def parse_expires(expires: str) -> datetime.date:
|
|||
Raises a ValueError in case the string is not properly formatted.
|
||||
"""
|
||||
try:
|
||||
if sys.version_info < (3, 7):
|
||||
try:
|
||||
return iso8601.parse_date(expires).date()
|
||||
except iso8601.ParseError:
|
||||
raise ValueError()
|
||||
else:
|
||||
return datetime.date.fromisoformat(expires)
|
||||
return date_fromisoformat(expires)
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"Invalid expiration date '{expires}'. "
|
||||
|
@ -407,6 +421,31 @@ def validate_expires(expires: str) -> None:
|
|||
)
|
||||
|
||||
|
||||
def build_date(date: Optional[str]) -> datetime.datetime:
|
||||
"""
|
||||
Generate the build timestamp.
|
||||
|
||||
If `date` is set to `0` a static unix epoch time will be used.
|
||||
If `date` it is set to a ISO8601 datetime string (e.g. `2022-01-03T17:30:00`)
|
||||
it will use that date.
|
||||
Note that any timezone offset will be ignored and UTC will be used.
|
||||
Otherwise it will throw an error.
|
||||
|
||||
If `date` is `None` it will use the current date & time.
|
||||
"""
|
||||
|
||||
if date is not None:
|
||||
date = str(date)
|
||||
if date == "0":
|
||||
ts = datetime.datetime(1970, 1, 1, 0, 0, 0)
|
||||
else:
|
||||
ts = datetime_fromisoformat(date).replace(tzinfo=datetime.timezone.utc)
|
||||
else:
|
||||
ts = datetime.datetime.utcnow()
|
||||
|
||||
return ts
|
||||
|
||||
|
||||
def report_validation_errors(all_objects):
|
||||
"""
|
||||
Report any validation errors found to the console.
|
||||
|
|
|
@ -96,6 +96,84 @@ def test_translate_no_buildinfo(tmpdir):
|
|||
assert "package Foo" in content
|
||||
|
||||
|
||||
def test_translate_build_date(tmpdir):
|
||||
"""Test with a custom build date."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
__main__.main,
|
||||
[
|
||||
"translate",
|
||||
str(ROOT / "data" / "core.yaml"),
|
||||
"-o",
|
||||
str(tmpdir),
|
||||
"-f",
|
||||
"kotlin",
|
||||
"-s",
|
||||
"namespace=Foo",
|
||||
"-s",
|
||||
"build_date=2020-01-01T17:30:00",
|
||||
"--allow-reserved",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
path = Path(str(tmpdir)) / "GleanBuildInfo.kt"
|
||||
with path.open(encoding="utf-8") as fd:
|
||||
content = fd.read()
|
||||
assert "buildDate = Calendar.getInstance" in content
|
||||
assert "cal.set(2020, 0, 1, 17, 30" in content
|
||||
|
||||
|
||||
def test_translate_fixed_build_date(tmpdir):
|
||||
"""Test with a custom build date."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
__main__.main,
|
||||
[
|
||||
"translate",
|
||||
str(ROOT / "data" / "core.yaml"),
|
||||
"-o",
|
||||
str(tmpdir),
|
||||
"-f",
|
||||
"kotlin",
|
||||
"-s",
|
||||
"namespace=Foo",
|
||||
"-s",
|
||||
"build_date=0",
|
||||
"--allow-reserved",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
path = Path(str(tmpdir)) / "GleanBuildInfo.kt"
|
||||
with path.open(encoding="utf-8") as fd:
|
||||
content = fd.read()
|
||||
assert "buildDate = Calendar.getInstance" in content
|
||||
assert "cal.set(1970" in content
|
||||
|
||||
|
||||
def test_translate_borked_build_date(tmpdir):
|
||||
"""Test with a custom build date."""
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
__main__.main,
|
||||
[
|
||||
"translate",
|
||||
str(ROOT / "data" / "core.yaml"),
|
||||
"-o",
|
||||
str(tmpdir),
|
||||
"-f",
|
||||
"kotlin",
|
||||
"-s",
|
||||
"namespace=Foo",
|
||||
"-s",
|
||||
"build_date=1",
|
||||
"--allow-reserved",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 1
|
||||
|
||||
|
||||
def test_translate_errors(tmpdir):
|
||||
"""Test the 'translate' command."""
|
||||
runner = CliRunner()
|
||||
|
|
|
@ -88,6 +88,10 @@ def test_parser(tmpdir):
|
|||
content = fd.read()
|
||||
assert 'category = ""' in content
|
||||
|
||||
with (tmpdir / "GleanBuildInfo.kt").open("r", encoding="utf-8") as fd:
|
||||
content = fd.read()
|
||||
assert "buildDate = Calendar.getInstance" in content
|
||||
|
||||
run_linters(tmpdir.glob("*.kt"))
|
||||
|
||||
|
||||
|
|
|
@ -49,6 +49,56 @@ def test_parser(tmpdir):
|
|||
assert "True if the user has set Firefox as the default browser." in content
|
||||
assert "جمع 搜集" in content
|
||||
assert 'category: ""' in content
|
||||
assert "class GleanBuild" in content
|
||||
assert "BuildInfo(buildDate:" in content
|
||||
|
||||
run_linters(tmpdir.glob("*.swift"))
|
||||
|
||||
|
||||
def test_parser_no_build_info(tmpdir):
|
||||
"""Test translating metrics to Swift files without build info."""
|
||||
tmpdir = Path(str(tmpdir))
|
||||
|
||||
translate.translate(
|
||||
ROOT / "data" / "core.yaml",
|
||||
"swift",
|
||||
tmpdir,
|
||||
{"with_buildinfo": "false"},
|
||||
{"allow_reserved": True},
|
||||
)
|
||||
|
||||
assert set(x.name for x in tmpdir.iterdir()) == set(["Metrics.swift"])
|
||||
|
||||
# Make sure descriptions made it in
|
||||
with (tmpdir / "Metrics.swift").open("r", encoding="utf-8") as fd:
|
||||
content = fd.read()
|
||||
|
||||
assert "class GleanBuild" not in content
|
||||
|
||||
run_linters(tmpdir.glob("*.swift"))
|
||||
|
||||
|
||||
def test_parser_custom_build_date(tmpdir):
|
||||
"""Test translating metrics to Swift files without build info."""
|
||||
tmpdir = Path(str(tmpdir))
|
||||
|
||||
translate.translate(
|
||||
ROOT / "data" / "core.yaml",
|
||||
"swift",
|
||||
tmpdir,
|
||||
{"build_date": "2020-01-01T17:30:00"},
|
||||
{"allow_reserved": True},
|
||||
)
|
||||
|
||||
assert set(x.name for x in tmpdir.iterdir()) == set(["Metrics.swift"])
|
||||
|
||||
# Make sure descriptions made it in
|
||||
with (tmpdir / "Metrics.swift").open("r", encoding="utf-8") as fd:
|
||||
content = fd.read()
|
||||
|
||||
assert "class GleanBuild" in content
|
||||
assert "BuildInfo(buildDate:" in content
|
||||
assert "year: 2020, month: 1, day: 1" in content
|
||||
|
||||
run_linters(tmpdir.glob("*.swift"))
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче