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

Add BuildpackOutput #721

Merged
merged 100 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
86fba2f
Port build output from Ruby build pack
Nov 6, 2023
a5843fc
[Stacked PR] Remove warn_later from output (#760)
schneems Jan 19, 2024
d4b83f9
Removes the boxed trait state machine pattern and replaces it with st…
schneems Jan 24, 2024
fafddee
Remove ReadYourWrite utility struct
schneems Jan 24, 2024
8127b19
Fix typos
Malax Jan 26, 2024
314742f
Draft: Slimmer build output PR experiment (#761)
Malax Jan 26, 2024
70ac8d0
Fix tests, address clippy lints
schneems Jan 26, 2024
80c28c3
Address `strip_trailing_whitespace` comments
schneems Jan 26, 2024
b55e2c7
Remove ascii_table dependency
schneems Jan 26, 2024
15c828e
Rename `build_log` module to `BuildpackOutput`
schneems Jan 26, 2024
4e2887f
Rename `InSection` to `Section`
schneems Jan 26, 2024
4e6f3d3
Fix spelling in test
schneems Jan 26, 2024
d677ee5
s/log/output for buildpack_output
schneems Jan 26, 2024
b9f6e2f
Replace announce struct with additional state
schneems Jan 26, 2024
153cd53
Hide docs for public but internal structures
schneems Jan 27, 2024
f4c210c
Remove BuildData
Malax Jan 29, 2024
9070ac2
Rustdoc touchups
Malax Jan 29, 2024
83a195a
Rename NOCOLOR to NO_COLOR
Malax Jan 29, 2024
a497bdb
Fix "miliseconds" typo
Malax Jan 29, 2024
537df68
Remove output module, rename output feature
Malax Jan 29, 2024
c8793a7
Add failing test and fix test spelling
schneems Jan 29, 2024
8ba3248
Add ParagraphInspectWrite
Malax Jan 29, 2024
ff79b30
Replace state::Announce with ParagraphInspectWrite
schneems Jan 29, 2024
d63cb52
Fix typo
Malax Jan 30, 2024
53db8f8
Remove infectious Debug trait bounds
Malax Jan 30, 2024
fc36f23
Replace `Stream` with `BuildpackOutput` state
Malax Jan 30, 2024
174fc02
Remove inline_output module
Malax Jan 30, 2024
81f63d0
Remove trim_end_lines
Malax Jan 30, 2024
f23b888
Refactor prefix_indent
Malax Jan 30, 2024
870ca33
Refactor style
Malax Jan 30, 2024
af7494c
Fix warning and double warning tests
schneems Jan 30, 2024
b3d498d
Fix header followed by warning tests
schneems Jan 30, 2024
367504c
Reduce clippy warnings
schneems Jan 30, 2024
08478b8
Refactor warn/important/error internals
schneems Jan 30, 2024
dd83998
Adjust visibility due to clippy
schneems Jan 30, 2024
644c5ae
Change &str to impl AsRef<str> for flexability
schneems Jan 30, 2024
f36c4b3
Move style logic closer to the end use
schneems Jan 30, 2024
b5b29e0
Rename TimedStream to Stream
schneems Jan 30, 2024
2f7a13e
Remove pretty_assertions
Malax Jan 31, 2024
910c619
Remove fun_run
Malax Jan 31, 2024
6f5c6d6
Move Duration formatting into dedicated file
Malax Jan 31, 2024
e5daaf4
Clean up duration_format.rs
Malax Jan 31, 2024
83dacfc
Remove unused pub constants
Malax Jan 31, 2024
3624627
Move constants to constants.rs
Malax Jan 31, 2024
bba601d
Move strip_control_codes closer to single usage
Malax Jan 31, 2024
549e73a
Remove single-use renamed constants
Malax Jan 31, 2024
7efe225
Refactor and rename strip_control_codes
Malax Jan 31, 2024
a534371
Move ANSI related code to separate module
Malax Jan 31, 2024
6b1cea9
Move prefix functions to util module
Malax Jan 31, 2024
3e3cc80
Remove wildcard import
Malax Jan 31, 2024
089964f
Remove const_format
Malax Jan 31, 2024
2d4fc79
Remove unnecessary must_use attributes
Malax Jan 31, 2024
b3ecc86
Simplify BuildpackOutput<state::Section<W>>::step
Malax Jan 31, 2024
17922c3
Remove BuildpackOutput<state::Section<W>>::step_mut
Malax Jan 31, 2024
3e029e7
Rename colorize_multiline to inject_default_ansi_escape, add docs
Malax Jan 31, 2024
075c468
Fix module docs for buildpack_output::style
Malax Jan 31, 2024
2f8b845
Rename all state finishing functions to "finish"
Malax Jan 31, 2024
390f330
Update README
Malax Jan 31, 2024
b4e877c
Apply suggestions from code review
schneems Feb 5, 2024
1c27a74
Update CHANGELOG.md to correct module name
schneems Feb 5, 2024
26a9b09
Follow rust style guides for .expect()
schneems Feb 5, 2024
e772758
Add doctests for `build_output::state`
schneems Feb 5, 2024
f31f660
Apply suggestions from code review
schneems Feb 5, 2024
a3571b6
Add BuildpackOutput docs
schneems Feb 6, 2024
0d8629b
Apply suggestions from code review
schneems Feb 7, 2024
7fd6408
Update date formatting
schneems Feb 7, 2024
5fdedb5
Add tests for nested ansi cases
schneems Feb 8, 2024
71b3af2
Remove unnecessary iterator
schneems Feb 8, 2024
b98c6a4
Make LockedWriter #[cfg(test)] move to bottom
schneems Feb 8, 2024
ad08878
Re-order functions
schneems Feb 8, 2024
220fa16
Update ANSI color docs
schneems Feb 8, 2024
63c08c8
Refactor inject_default_ansi_escape to take enum
schneems Feb 8, 2024
bd7f2cd
Rename function
schneems Feb 8, 2024
6993046
Move module docs from file to the code
schneems Feb 9, 2024
f620ce3
Apply suggestions from code review
schneems Feb 11, 2024
5993033
Add warning language
schneems Feb 11, 2024
5b88969
Specify various streaming states must be used
schneems Feb 11, 2024
912e53f
Test important and error buildpack output
schneems Feb 11, 2024
3240465
Test color codes for warning/important/etc.
schneems Feb 11, 2024
6abd930
Remove confusing fmt::Write usage
schneems Feb 11, 2024
167cdc7
Add docs and tests for ParagraphInspectWrite
schneems Feb 11, 2024
ddb6bbf
Assert prefix_first_rest_lines preserves inner newlines
schneems Feb 11, 2024
8be7f90
Add test for empty line case
schneems Feb 12, 2024
acf3f77
Apply suggestions from code review
schneems Feb 12, 2024
ab5d9a5
Document, test, and refactor prefix functions
schneems Feb 12, 2024
f3f6e39
Remove `log` and `error` features
schneems Feb 12, 2024
fd2a647
Merge branch 'main' into schneems/output
schneems Feb 12, 2024
a76c490
Update changelog
schneems Feb 12, 2024
55868b3
Revert removing error, only remove log
schneems Feb 13, 2024
1da805e
Revert removing `log` and changes to `on_error`
edmorley Feb 13, 2024
16c90e6
Remove stray README newline
edmorley Feb 13, 2024
7d8fa16
Revert the implementation change in ab5d9a5579b3d9b1a7b54da80f27c9134…
edmorley Feb 13, 2024
7185440
Remove stray word from CHANGELOG
edmorley Feb 13, 2024
5e20c41
Fix `write_paragraph`'s handling of prefixes for empty lines
edmorley Feb 13, 2024
3ad2008
Sanitize stray newlines at point of origin
schneems Feb 13, 2024
aa19940
Update testcase to cover trailing newlines in another case
edmorley Feb 14, 2024
67e67aa
Add a test for streaming with blank lines and a trailing newline
edmorley Feb 14, 2024
d423789
Fix the `line_mapped` closure used in `start_stream`
edmorley Feb 14, 2024
29f1a4e
Make line wrapping of rustdocs more consistent
edmorley Feb 14, 2024
e177d78
Fix double newline after streaming
schneems Feb 14, 2024
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `libherokubuildpack`:
- Added `buildpack_output` module. This will help buildpack authors provide consistent and delightful output to their buildpack users ([#721](https://github.com/heroku/libcnb.rs/pull/721))

## [0.18.0] - 2024-02-12

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ That's all we need! We can now move on to finally write some buildpack code!

### Writing the Buildpack

The buildpack we're writing will be very simple. We will just log a "Hello World" message during the build
The buildpack we're writing will be very simple. We will just output a "Hello World" message during the build
and set the default process type to a command that will also emit "Hello World" when the application image is run.
Examples of more complex buildpacks can be found in the [examples directory](https://github.com/heroku/libcnb.rs/tree/main/examples).

Expand Down
5 changes: 4 additions & 1 deletion libherokubuildpack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ all-features = true
workspace = true

[features]
default = ["command", "download", "digest", "error", "log", "tar", "toml", "fs", "write"]
default = ["command", "download", "digest", "error", "log", "tar", "toml", "fs", "write", "buildpack_output"]
download = ["dep:ureq", "dep:thiserror"]
digest = ["dep:sha2"]
error = ["log", "dep:libcnb"]
Expand All @@ -27,6 +27,7 @@ tar = ["dep:tar", "dep:flate2"]
toml = ["dep:toml"]
fs = ["dep:pathdiff"]
command = ["write", "dep:crossbeam-utils"]
buildpack_output = []
write = []

[dependencies]
Expand All @@ -47,4 +48,6 @@ toml = { workspace = true, optional = true }
ureq = { version = "2.9.5", default-features = false, features = ["tls"], optional = true }

[dev-dependencies]
indoc = "2.0.4"
libcnb-test = { workspace = true }
tempfile = "3.10.0"
2 changes: 2 additions & 0 deletions libherokubuildpack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ The feature names line up with the modules in this crate. All features are enabl
Enables helpers to achieve consistent error logging.
* **log** -
Enables helpers for logging.
* **buildpack_output** -
Enables helpers for user-facing buildpack output.
* **tar** -
Enables helpers for working with tarballs.
* **toml** -
Expand Down
115 changes: 115 additions & 0 deletions libherokubuildpack/src/buildpack_output/ansi_escape.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/// Wraps each line in an ANSI escape sequence while preserving prior ANSI escape sequences.
///
/// ## Why does this exist?
///
/// When buildpack output is streamed to the user, each line is prefixed with `remote: ` by Git.
/// Any colorization of text will apply to those prefixes which is not the desired behavior. This
/// function colors lines of text while ensuring that styles are disabled at the end of each line.
///
/// ## Supports recursive colorization
///
/// Strings that are previously colorized will not be overridden by this function. For example,
/// if a word is already colored yellow, that word will continue to be yellow.
pub(crate) fn wrap_ansi_escape_each_line(ansi: &ANSI, body: impl AsRef<str>) -> String {
let ansi_escape = ansi.to_str();
body.as_ref()
.split('\n')
// If sub contents are colorized it will contain SUBCOLOR ... RESET. After the reset,
// ensure we change back to the current color
.map(|line| line.replace(RESET, &format!("{RESET}{ansi_escape}"))) // Handles nested color
// Set the main color for each line and reset after so we don't colorize `remote:` by accident
.map(|line| format!("{ansi_escape}{line}{RESET}"))
// The above logic causes redundant colors and resets, clean them up
.map(|line| line.replace(&format!("{ansi_escape}{ansi_escape}"), ansi_escape)) // Reduce useless color
.map(|line| line.replace(&format!("{ansi_escape}{RESET}"), "")) // Empty lines or where the nested color is at the end of the line
.collect::<Vec<String>>()
.join("\n")
}

const RESET: &str = "\x1B[0m";
const RED: &str = "\x1B[0;31m";
const YELLOW: &str = "\x1B[0;33m";
const BOLD_CYAN: &str = "\x1B[1;36m";
const BOLD_PURPLE: &str = "\x1B[1;35m";

#[derive(Debug)]
#[allow(clippy::upper_case_acronyms)]
pub(crate) enum ANSI {
Red,
Yellow,
BoldCyan,
BoldPurple,
}
edmorley marked this conversation as resolved.
Show resolved Hide resolved

impl ANSI {
fn to_str(&self) -> &'static str {
match self {
ANSI::Red => RED,
ANSI::Yellow => YELLOW,
ANSI::BoldCyan => BOLD_CYAN,
ANSI::BoldPurple => BOLD_PURPLE,
}
}
}

#[cfg(test)]
mod test {
schneems marked this conversation as resolved.
Show resolved Hide resolved
use super::*;

#[test]
fn empty_line() {
let actual = wrap_ansi_escape_each_line(&ANSI::Red, "\n");
let expected = String::from("\n");
assert_eq!(expected, actual);
}

#[test]
fn handles_nested_color_at_start() {
let start = wrap_ansi_escape_each_line(&ANSI::BoldCyan, "hello");
let out = wrap_ansi_escape_each_line(&ANSI::Red, format!("{start} world"));
let expected = format!("{RED}{BOLD_CYAN}hello{RESET}{RED} world{RESET}");

assert_eq!(expected, out);
}

#[test]
fn handles_nested_color_in_middle() {
let middle = wrap_ansi_escape_each_line(&ANSI::BoldCyan, "middle");
let out = wrap_ansi_escape_each_line(&ANSI::Red, format!("hello {middle} color"));
let expected = format!("{RED}hello {BOLD_CYAN}middle{RESET}{RED} color{RESET}");
assert_eq!(expected, out);
}

#[test]
fn handles_nested_color_at_end() {
let end = wrap_ansi_escape_each_line(&ANSI::BoldCyan, "world");
let out = wrap_ansi_escape_each_line(&ANSI::Red, format!("hello {end}"));
let expected = format!("{RED}hello {BOLD_CYAN}world{RESET}");

assert_eq!(expected, out);
}

#[test]
fn handles_double_nested_color() {
let inner = wrap_ansi_escape_each_line(&ANSI::BoldCyan, "inner");
let outer = wrap_ansi_escape_each_line(&ANSI::Red, format!("outer {inner}"));
let out = wrap_ansi_escape_each_line(&ANSI::Yellow, format!("hello {outer}"));
let expected = format!("{YELLOW}hello {RED}outer {BOLD_CYAN}inner{RESET}");

assert_eq!(expected, out);
}

#[test]
fn splits_newlines() {
let actual = wrap_ansi_escape_each_line(&ANSI::Red, "hello\nworld");
let expected = format!("{RED}hello{RESET}\n{RED}world{RESET}");

assert_eq!(expected, actual);
}

#[test]
fn simple_case() {
let actual = wrap_ansi_escape_each_line(&ANSI::Red, "hello world");
assert_eq!(format!("{RED}hello world{RESET}"), actual);
}
}
66 changes: 66 additions & 0 deletions libherokubuildpack/src/buildpack_output/duration_format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::time::Duration;

pub(crate) fn human(duration: &Duration) -> String {
schneems marked this conversation as resolved.
Show resolved Hide resolved
let hours = (duration.as_secs() / 3600) % 60;
let minutes = (duration.as_secs() / 60) % 60;
let seconds = duration.as_secs() % 60;
let milliseconds = duration.subsec_millis();
let tenths = milliseconds / 100;

if hours > 0 {
format!("{hours}h {minutes}m {seconds}s")
} else if minutes > 0 {
format!("{minutes}m {seconds}s")
} else if seconds > 0 || milliseconds >= 100 {
format!("{seconds}.{tenths}s")
} else {
String::from("< 0.1s")
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_display_duration() {
schneems marked this conversation as resolved.
Show resolved Hide resolved
let duration = Duration::ZERO;
assert_eq!(human(&duration), "< 0.1s");

let duration = Duration::from_millis(99);
assert_eq!(human(&duration), "< 0.1s");

let duration = Duration::from_millis(100);
assert_eq!(human(&duration), "0.1s");

let duration = Duration::from_millis(210);
assert_eq!(human(&duration), "0.2s");

let duration = Duration::from_millis(1100);
assert_eq!(human(&duration), "1.1s");

let duration = Duration::from_millis(9100);
assert_eq!(human(&duration), "9.1s");

let duration = Duration::from_millis(10100);
assert_eq!(human(&duration), "10.1s");

let duration = Duration::from_millis(52100);
assert_eq!(human(&duration), "52.1s");

let duration = Duration::from_millis(60 * 1000);
assert_eq!(human(&duration), "1m 0s");

let duration = Duration::from_millis(60 * 1000 + 2000);
assert_eq!(human(&duration), "1m 2s");

let duration = Duration::from_millis(60 * 60 * 1000 - 1);
assert_eq!(human(&duration), "59m 59s");

let duration = Duration::from_millis(60 * 60 * 1000);
assert_eq!(human(&duration), "1h 0m 0s");

let duration = Duration::from_millis(75 * 60 * 1000 - 1);
assert_eq!(human(&duration), "1h 14m 59s");
}
}
Loading