cranpose-services 0.1.163

Multiplatform system services for Cranpose (HTTP, URI, and OS integrations)
Documentation
#[test]
fn a_uri_that_cannot_be_decoded_exactly_is_refused_but_still_displays() {
    assert_eq!(
        percent_decode("Trip%20Photos").as_deref(),
        Some("Trip Photos")
    );
    assert_eq!(percent_decode_lossy("Trip%20Photos"), "Trip Photos");

    assert_eq!(percent_decode("bad%FFname"), None);
    assert_eq!(percent_decode_lossy("bad%FFname"), "bad\u{fffd}name");

    assert_eq!(percent_decode("cut%4"), None);
    assert_eq!(percent_decode_lossy("cut%4"), "cut%4");

    assert_eq!(percent_decode("100%zz"), None);
    assert_eq!(percent_decode_lossy("100%zz"), "100%zz");
}

use super::*;

fn block<T>(future: impl Future<Output = T>) -> T {
    pollster::block_on(future)
}

#[test]
fn metadata_reports_the_lowercase_extension() {
    let metadata = ContentMetadata::named("Scan.PNG");
    assert_eq!(metadata.extension().as_deref(), Some("png"));
    assert_eq!(ContentMetadata::named("noext").extension(), None);
    assert_eq!(ContentMetadata::named("trailing.").extension(), None);
}

#[test]
fn metadata_carries_what_a_provider_knew_about_the_item() {
    let metadata = ContentMetadata::named("Scan.png")
        .with_len(2_048)
        .with_modified_millis(1_700_000_000_000)
        .with_identifier("content://provider/17")
        .with_mime_type("image/png");

    assert_eq!(metadata.name, "Scan.png");
    assert_eq!(metadata.len, Some(2_048));
    assert_eq!(metadata.modified_millis, Some(1_700_000_000_000));
    assert_eq!(metadata.identifier, "content://provider/17");
    assert_eq!(metadata.mime_type.as_deref(), Some("image/png"));
    let bare = ContentMetadata::named("Scan.png");
    assert_eq!(bare.len, None);
    assert_eq!(bare.modified_millis, None);
    assert_eq!(bare.mime_type, None);
    assert_eq!(bare.identifier, "Scan.png");
}

#[test]
fn a_channel_reports_when_it_has_ended_and_ignores_what_arrives_after() {
    let channel = ContentChannel::new();
    assert!(!channel.is_closed());

    channel.close();
    assert!(channel.is_closed());

    channel.push(BytesContent::named("late.bin", vec![1]).handle());
    channel.fail(ContentError::Unsupported("after close"));
    assert!(channel.is_closed());

    let stream = channel.stream();
    assert!(
        block(stream.next())
            .expect("a closed channel ends cleanly")
            .is_none(),
        "a channel closed before anything was pushed must yield nothing"
    );
}

#[test]
fn bytes_content_streams_in_chunks_and_reads_whole() {
    let payload = vec![7u8; DEFAULT_CHUNK_LEN + 11];
    let content = BytesContent::named("blob.bin", payload.clone()).handle();
    assert_eq!(content.metadata().len, Some(payload.len() as u64));

    let chunks = block(async {
        let reader = content.open().await.unwrap();
        let mut sizes = Vec::new();
        while let Some(chunk) = reader.read_chunk().await.unwrap() {
            sizes.push(chunk.len());
        }
        sizes
    });
    assert_eq!(chunks, vec![DEFAULT_CHUNK_LEN, 11]);
    assert_eq!(block(content.read_all()).unwrap(), payload);
}

#[test]
fn walking_a_tree_yields_every_file_depth_first() {
    let nested = ReadyFolder::new(
        ContentMetadata::named("nested"),
        vec![ContentEntry::File(
            BytesContent::named("b.txt", b"b".to_vec()).handle(),
        )],
    )
    .handle();
    let root = ReadyFolder::new(
        ContentMetadata::named("root"),
        vec![
            ContentEntry::File(BytesContent::named("a.txt", b"a".to_vec()).handle()),
            ContentEntry::Folder(nested),
        ],
    )
    .handle();

    let stream = folder_files(root);
    let files = block(collect_stream(&stream)).unwrap();
    let names: Vec<String> = files.iter().map(|file| file.metadata().name).collect();
    assert_eq!(names, vec!["a.txt", "b.txt"]);
    assert_eq!(stream.produced(), Some(2));
}

#[test]
fn a_channel_wakes_its_collector_instead_of_being_polled() {
    let channel = Rc::new(ContentChannel::new());
    let stream = channel.stream();

    let mut future = Box::pin(stream.next());
    let waker = Waker::noop().clone();
    let mut context = Context::from_waker(&waker);
    assert!(future.as_mut().poll(&mut context).is_pending());

    channel.push(BytesContent::named("late.txt", b"late".to_vec()).handle());
    let Poll::Ready(Ok(Some(item))) = future.as_mut().poll(&mut context) else {
        panic!("the pushed item should have completed the pending collector");
    };
    assert_eq!(item.metadata().name, "late.txt");

    channel.close();
    assert!(matches!(block(stream.next()), Ok(None)));
    assert_eq!(stream.produced(), Some(1));
}

#[test]
fn a_failed_channel_reports_the_error_once_then_ends() {
    let channel = ContentChannel::new();
    let stream = channel.stream();
    channel.fail(ContentError::Io("provider died".into()));
    let Err(failure) = block(stream.next()) else {
        panic!("a failed channel reports its error to the collector");
    };
    assert_eq!(failure, ContentError::Io("provider died".into()));
    assert!(matches!(block(stream.next()), Ok(None)));
}