Skip to content
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
15 changes: 15 additions & 0 deletions .github/actions/use-ci-release/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ runs:
using: composite
steps:
- name: Download release artifact
id: download
continue-on-error: true
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: moli-release-${{ inputs.side }}
path: target/ci-artifacts/${{ inputs.side }}
- name: Check downloaded archive completeness
id: archive
continue-on-error: true
shell: bash
env:
RELEASE_SIDE: ${{ inputs.side }}
run: gzip -t "target/ci-artifacts/$RELEASE_SIDE/moli-release-$RELEASE_SIDE.tar.gz"
- name: Retry incomplete release download
if: steps.download.outcome != 'success' || steps.archive.outcome != 'success'
Comment thread
lanyue-llk marked this conversation as resolved.
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: moli-release-${{ inputs.side }}
Expand Down
3 changes: 3 additions & 0 deletions .github/scripts/unpack-ci-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ esac
release_name="moli-release-$side"
archive="target/ci-artifacts/$side/$release_name.tar.gz"
test -f "$archive"
# A download action can terminate with a partial extracted archive. Reject it
# before touching the executable directory, including after the bounded retry.
gzip -t "$archive"
mkdir -p target/ci-bin
tar -xzf "$archive" -C target/ci-bin
(cd "target/ci-bin/$release_name" && sha256sum --check SHA256SUMS)
Expand Down
60 changes: 60 additions & 0 deletions .github/scripts/unpack-ci-release.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use strict';

const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const { createHash } = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');

const script = path.join(__dirname, 'unpack-ci-release.sh');

function fixture(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'moli-ci-release-test-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const packageDir = path.join(root, 'package', 'moli-release-head');
const archive = path.join(root, 'target', 'ci-artifacts', 'head', 'moli-release-head.tar.gz');
fs.mkdirSync(packageDir, { recursive: true });
fs.mkdirSync(path.dirname(archive), { recursive: true });
const binary = Buffer.from('test release binary');
fs.writeFileSync(path.join(packageDir, 'moli'), binary);
fs.writeFileSync(path.join(packageDir, 'revision.txt'), 'expected-revision\n');
fs.writeFileSync(path.join(packageDir, 'SHA256SUMS'),
`${createHash('sha256').update(binary).digest('hex')} moli\n`);
function pack() {
const result = spawnSync('tar', ['-czf', archive, '-C', path.join(root, 'package'), 'moli-release-head']);
assert.equal(result.status, 0, String(result.stderr));
}
pack();
return { root, archive, packageDir, pack };
}

function unpack(root, revision = 'expected-revision') {
return spawnSync('bash', [script, 'head', revision], { cwd: root, encoding: 'utf8' });
}

test('valid archive passes payload and exact revision verification', (t) => {
const { root } = fixture(t);
const result = unpack(root);
assert.equal(result.status, 0, result.stderr);
assert.equal(fs.readFileSync(path.join(root, 'target/ci-bin/moli-release-head/moli'), 'utf8'), 'test release binary');
});

test('truncated download cannot write a partial executable; redownload recovers', (t) => {
const { root, archive } = fixture(t);
const complete = fs.readFileSync(archive);
fs.writeFileSync(archive, complete.subarray(0, complete.length - 16));
assert.notEqual(unpack(root).status, 0);
assert.equal(fs.existsSync(path.join(root, 'target/ci-bin')), false);
fs.writeFileSync(archive, complete);
assert.equal(unpack(root).status, 0);
});

test('complete downloads still reject wrong revisions and altered payloads', (t) => {
const { root, packageDir, pack } = fixture(t);
assert.notEqual(unpack(root, 'wrong-revision').status, 0);
fs.writeFileSync(path.join(packageDir, 'moli'), 'tampered payload');
pack();
assert.notEqual(unpack(root).status, 0);
});
3 changes: 2 additions & 1 deletion .github/workflows/cancel-pr-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Record cancellation
run: echo "Cancelled validation for closed PR #${{ github.event.pull_request.number }}."
run: |
echo "Cancelled validation for closed PR #${{ github.event.pull_request.number }}."
73 changes: 72 additions & 1 deletion moli-fetch/src/tests/support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1003,14 +1003,85 @@ impl ScriptedHttpServer {
}
}

#[test]
fn scripted_server_waits_for_request_on_nonblocking_accepted_socket() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
client
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
let (stream, _) = listener.accept().unwrap();
// macOS may inherit this mode from the listening socket.
stream.set_nonblocking(true).unwrap();
let hits = Arc::new(AtomicUsize::new(0));
let requests = Arc::new(Mutex::new(Vec::new()));
let responses = Arc::new(Mutex::new(VecDeque::from([ScriptedResponse::ok("ready")])));
let (done_tx, done_rx) = std_mpsc::channel();
let handler = {
let hits = Arc::clone(&hits);
let requests = Arc::clone(&requests);
thread::spawn(move || {
handle_scripted_connection(stream, hits, requests, responses);
done_tx.send(()).unwrap();
})
};
assert!(
matches!(
done_rx.recv_timeout(Duration::from_millis(100)),
Err(std_mpsc::RecvTimeoutError::Timeout)
),
"a connection without HTTP bytes must not receive a scripted response"
);
client
.write_all(b"GET /delayed HTTP/1.1\r\nHost: localhost\r\n\r\n")
.unwrap();
let mut response = String::new();
client.read_to_string(&mut response).unwrap();
handler.join().unwrap();
assert!(response.ends_with("ready"));
assert_eq!(hits.load(Ordering::SeqCst), 1);
assert!(requests.lock()[0].starts_with("GET /delayed HTTP/1.1\r\n"));
}

#[test]
fn scripted_server_does_not_consume_response_for_closed_unused_connection() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
let (stream, _) = listener.accept().unwrap();
drop(client);
let hits = Arc::new(AtomicUsize::new(0));
let requests = Arc::new(Mutex::new(Vec::new()));
let responses = Arc::new(Mutex::new(VecDeque::from([ScriptedResponse::ok("ready")])));
handle_scripted_connection(
stream,
Arc::clone(&hits),
Arc::clone(&requests),
Arc::clone(&responses),
);
assert_eq!(hits.load(Ordering::SeqCst), 0);
assert!(requests.lock().is_empty());
assert_eq!(responses.lock().len(), 1);
}

fn handle_scripted_connection(
mut stream: std::net::TcpStream,
hits: Arc<AtomicUsize>,
requests: Arc<Mutex<Vec<String>>>,
responses: Arc<Mutex<VecDeque<ScriptedResponse>>>,
) {
stream
.set_nonblocking(false)
.expect("scripted HTTP connection should use blocking reads");
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("scripted HTTP connection should have a bounded read");
let mut request = [0; 1024];
let bytes_read = stream.read(&mut request).unwrap_or(0);
let bytes_read = stream
.read(&mut request)
.expect("read scripted HTTP request");
if bytes_read == 0 {
return;
}
let request_text = String::from_utf8_lossy(&request[..bytes_read]).into_owned();
requests.lock().push(request_text);
let _ = hits.fetch_add(1, Ordering::SeqCst) + 1;
Expand Down
13 changes: 13 additions & 0 deletions moli-layout/tests/phase4_layout_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,19 @@ fn inline_blocks_use_their_internal_last_line_baseline_and_overflow_fallback() {
);
assert_close(baselines[0], more.y + more.height);
assert_close(baselines[1], more.y + more.height);

// Extra block-end padding moves the text-backed peers by the same amount
// while leaving the clipped inline-block's top edge fixed.
let fallback_news = rect(&fallback, RED);
let fallback_hao = rect(&fallback, GREEN);
styles
.primary
.insert(5, nav_item_style(BLUE, 29.0, Overflow::Hidden));
let deeper_fallback = render(&source, &mut styles, 300, 100);
assert_close(rect(&deeper_fallback, RED).y, fallback_news.y + 10.0);
assert_close(rect(&deeper_fallback, GREEN).y, fallback_hao.y + 10.0);
assert_close(rect(&deeper_fallback, BLUE).y, more.y);
assert_close(rect(&deeper_fallback, BLUE).height, more.height + 10.0);
}

#[test]
Expand Down
85 changes: 78 additions & 7 deletions moli-layout/tests/phase5_output_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -803,12 +803,48 @@ fn auto_scrollbar_feedback_reveals_the_perpendicular_axis() {
assert!(extent.vertical_scrollbar.is_some());
}

fn fixed_inline_font() -> (ResolvedLayoutStyle, DocumentLayoutServices) {
use style::values::computed::font::{
FamilyName, FontFamily, FontFamilyList, FontFamilyNameSyntax, SingleFontFamily,
};
let mut font = style::properties::style_structs::Font::initial_values();
font.set_font_family(FontFamily {
families: FontFamilyList {
list: style::ArcSlice::from_iter(std::iter::once(SingleFontFamily::FamilyName(
FamilyName {
name: Atom::from("Moli Ahem"),
syntax: FontFamilyNameSyntax::Quoted,
},
))),
},
is_system_font: false,
is_initial: false,
});
let style = ResolvedLayoutStyle::from_stylo(
style::properties::ComputedValues::initial_values_with_font_override(font),
);
let mut services =
DocumentLayoutServices::with_system_font_policy(moli_layout::SystemFontPolicy::Disabled);
services
.register_web_font(moli_layout::WebFontRegistration::new(
"fixed",
moli_layout::WebFontFace::new("Moli Ahem"),
include_bytes!("fixtures/moli-ahem.ttf").to_vec(),
))
.unwrap();
(style, services)
}

#[test]
fn scrollbar_feedback_rebreaks_the_reused_inline_layout_at_its_final_width() {
const TEXT: &str = "alpha beta gamma delta epsilon zeta eta theta iota kappa";
const TEXT: &str = concat!(
"alpha beta gamma delta epsilon zeta eta theta iota kappa ",
"supercalifragilisticexpialidocious",
);
let source = Source(vec![
Node::element("root", vec![1]),
Node::element("scroller", vec![2]),
Node::element("fixed-font", vec![3]),
Node::text("text", TEXT),
]);
let mut styles = Styles::default();
Expand All @@ -825,8 +861,9 @@ fn scrollbar_feedback_rebreaks_the_reused_inline_layout_at_its_final_width() {
height: length(40.0),
},
overflow: Point {
// Only vertical feedback is needed to narrow the text.
// Horizontal overflow depends on platform font advances.
// Only the vertical scrollbar narrows the text. Keep the
// long word's horizontal overflow without showing a
// horizontal scrollbar.
x: Overflow::Hidden,
y: Overflow::Scroll,
},
Expand All @@ -835,13 +872,32 @@ fn scrollbar_feedback_rebreaks_the_reused_inline_layout_at_its_final_width() {
),
);

let feedback = build(&source, &mut styles);
// The old system-font fixture happened to fit on macOS but exposed
// hanging-space overflow on Linux. Bind shaping to the same test face.
let (font_style, mut fixed_services) = fixed_inline_font();
styles.0.insert(2, font_style);
let feedback = build_layout_pass(
&source,
&mut styles,
&mut fixed_services,
LayoutPassRequest::new(LayoutViewport::new(320, 240, 1.0), LayoutFlushReason::Test),
)
.unwrap();
assert_eq!(feedback.metrics.numeric_layout_pass_count, 2);
assert_eq!(
feedback.element_metrics_for_source(1).unwrap().client_size,
moli_layout::LayoutSize::new(85.0, 40.0),
);
let feedback_text = feedback.client_rects_for_source(2);
let scroller_box = feedback.source_output(1).unwrap().principal_box.unwrap();
let extent = feedback.scroll_extent(scroller_box).unwrap();
assert!(extent.vertical_scrollbar.is_some());
assert!(extent.horizontal_scrollbar.is_none());
assert!(!extent.allows_user_scroll_x);
let feedback_text = feedback.text_range_rects(3, 0..TEXT.encode_utf16().count());
assert!(
!feedback_text.is_empty(),
"compare real text fragments, not element-only client rects"
);

// Lay out the same paragraph directly at the converged 85px content
// width. Its line fragments must match the scrollbar-corrected result;
Expand All @@ -859,8 +915,23 @@ fn scrollbar_feedback_rebreaks_the_reused_inline_layout_at_its_final_width() {
},
),
);
let direct = build(&source, &mut styles);
assert_eq!(feedback_text, direct.client_rects_for_source(2));
let direct = build_layout_pass(
&source,
&mut styles,
&mut fixed_services,
LayoutPassRequest::new(LayoutViewport::new(320, 240, 1.0), LayoutFlushReason::Test),
)
.unwrap();
assert_eq!(
feedback_text,
direct.text_range_rects(3, 0..TEXT.encode_utf16().count())
);
assert!(
feedback_text
.iter()
.any(|quad| quad.points.iter().any(|point| point.x > 85.0)),
"the unbreakable final word should retain horizontal overflow"
);
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,8 +608,9 @@ async fn worker_sync_xhr_timeout_cancels_fetch_and_throws_without_progress_event
r#"{{"error":{{"name":"TimeoutError","message":"Failed to execute 'send' on 'XMLHttpRequest': Failed to load '{base_url}/worker/slow.txt'.","isDomException":true}},"readyState":4,"status":0,"responseText":"","events":["readystatechange:1"]}}"#
)
);
server
timeout(TIMEOUT, server)
.await
.expect("slow sync worker xhr server should finish within test timeout")
.expect("slow sync worker xhr server should finish");
}

Expand Down
28 changes: 28 additions & 0 deletions moli/src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub fn init(log_filter: &str) {
.with_env_filter(filter)
.with_target(false)
.with_writer(std::io::stderr)
.log_internal_errors(false)
.try_init();
}

Expand Down Expand Up @@ -107,4 +108,31 @@ mod tests {
assert!(output.contains("fallback-info"));
assert!(!output.contains("dependency-warning"));
}

#[test]
fn broken_diagnostic_writer_does_not_panic() {
const CHILD: &str = "MOLI_TEST_BROKEN_DIAGNOSTIC_SINK";
if std::env::var_os(CHILD).is_some() {
super::init("error");
tracing::error!("the diagnostic receiver has already exited");
return;
}
let (reader, writer) = std::io::pipe().unwrap();
drop(reader);
let status = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"telemetry::tests::broken_diagnostic_writer_does_not_panic",
"--nocapture",
])
.env(CHILD, "1")
.stdout(std::process::Stdio::null())
.stderr(writer)
.status()
.unwrap();
assert!(
status.success(),
"logging to a closed diagnostic pipe terminated the process: {status}"
);
}
}
Loading
Loading