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

Bug 1742448 - Generate build date at invocation time #431

Merged
merged 1 commit into from
Jan 12, 2022
Merged
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
31 changes: 31 additions & 0 deletions glean_parser/kotlin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 = {}
Expand All @@ -257,13 +285,15 @@ 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(
"kotlin.buildinfo.jinja2",
)

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:
Expand All @@ -272,6 +302,7 @@ def output_kotlin(
namespace=namespace,
namespace_package=namespace_package,
glean_namespace=glean_namespace,
build_date=build_date,
)
)
fd.write("\n")
Expand Down
42 changes: 42 additions & 0 deletions glean_parser/swift.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
badboy marked this conversation as resolved.
Show resolved Hide resolved
("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.
Expand All @@ -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".
badboy marked this conversation as resolved.
Show resolved Hide resolved
- 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 = {}
Expand All @@ -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
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion glean_parser/templates/kotlin.buildinfo.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
)
}
}
10 changes: 10 additions & 0 deletions glean_parser/templates/swift.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -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 }} {
Expand Down
53 changes: 46 additions & 7 deletions glean_parser/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@
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

Expand Down Expand Up @@ -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}'. "
Expand Down Expand Up @@ -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.
badboy marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down
78 changes: 78 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions tests/test_kotlin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))


Expand Down
Loading