svn 0.1.8

Async Rust SVN client for Subversion svn://, svn+ssh://, and ra_svn workflows.
Documentation
use super::*;

#[test]
fn get_latest_rev_sends_command_and_parses_response() {
    run_async(async {
        let (mut session, mut server) = connected_session().await;

        let expected = SvnItem::List(vec![
            SvnItem::Word("get-latest-rev".to_string()),
            SvnItem::List(Vec::new()),
        ]);

        let server_task = tokio::spawn(async move {
            assert_eq!(read_line(&mut server).await, encode_line(&expected));
            write_item_line(&mut server, &auth_request("realm")).await;
            write_item_line(
                &mut server,
                &SvnItem::List(vec![
                    SvnItem::Word("success".to_string()),
                    SvnItem::List(vec![SvnItem::Number(42)]),
                ]),
            )
            .await;
        });

        let rev = session.get_latest_rev().await.unwrap();
        assert_eq!(rev, 42);

        server_task.await.unwrap();
    });
}

#[test]
fn get_latest_rev_reconnects_and_retries_on_unexpected_eof() {
    run_async(async {
        use std::sync::Arc;
        use std::sync::atomic::{AtomicUsize, Ordering};

        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();

        let accepted = Arc::new(AtomicUsize::new(0));
        let accepted_task = {
            let accepted = accepted.clone();
            tokio::spawn(async move {
                loop {
                    let (mut server, _) = listener.accept().await.unwrap();
                    let attempt = accepted.fetch_add(1, Ordering::SeqCst);

                    handshake_no_auth(&mut server).await;

                    let expected = SvnItem::List(vec![
                        SvnItem::Word("get-latest-rev".to_string()),
                        SvnItem::List(Vec::new()),
                    ]);
                    assert_eq!(read_line(&mut server).await, encode_line(&expected));
                    write_item_line(&mut server, &auth_request("realm")).await;

                    if attempt == 0 {
                        // Drop the connection before sending the command response to force an EOF.
                        continue;
                    }

                    write_item_line(
                        &mut server,
                        &SvnItem::List(vec![
                            SvnItem::Word("success".to_string()),
                            SvnItem::List(vec![SvnItem::Number(42)]),
                        ]),
                    )
                    .await;
                    break;
                }
            })
        };

        let url = SvnUrl::parse(&format!("svn://127.0.0.1:{}/repo", addr.port())).unwrap();
        let client = RaSvnClient::new(url, None, None)
            .with_connect_timeout(Duration::from_secs(1))
            .with_read_timeout(Duration::from_secs(1))
            .with_write_timeout(Duration::from_secs(1))
            .with_reconnect_retries(1);
        let mut session = client.open_session().await.unwrap();

        let rev = session.get_latest_rev().await.unwrap();
        assert_eq!(rev, 42);

        accepted_task.await.unwrap();
        assert_eq!(accepted.load(Ordering::SeqCst), 2);
    });
}

#[test]
fn get_dated_rev_sends_command_and_parses_response() {
    run_async(async {
        let (mut session, mut server) = connected_session().await;

        let expected = SvnItem::List(vec![
            SvnItem::Word("get-dated-rev".to_string()),
            SvnItem::List(vec![SvnItem::String(b"2025-01-01T00:00:00Z".to_vec())]),
        ]);

        let server_task = tokio::spawn(async move {
            assert_eq!(read_line(&mut server).await, encode_line(&expected));
            write_item_line(&mut server, &auth_request("realm")).await;
            write_item_line(
                &mut server,
                &SvnItem::List(vec![
                    SvnItem::Word("success".to_string()),
                    SvnItem::List(vec![SvnItem::Number(7)]),
                ]),
            )
            .await;
        });

        let rev = session.get_dated_rev("2025-01-01T00:00:00Z").await.unwrap();
        assert_eq!(rev, 7);

        server_task.await.unwrap();
    });
}

#[test]
fn rev_proplist_and_rev_prop_round_trip() {
    run_async(async {
        let (mut session, mut server) = connected_session().await;

        let expected_rev_proplist = SvnItem::List(vec![
            SvnItem::Word("rev-proplist".to_string()),
            SvnItem::List(vec![SvnItem::Number(5)]),
        ]);
        let expected_rev_prop = SvnItem::List(vec![
            SvnItem::Word("rev-prop".to_string()),
            SvnItem::List(vec![
                SvnItem::Number(5),
                SvnItem::String(b"svn:log".to_vec()),
            ]),
        ]);

        let server_task = tokio::spawn(async move {
            assert_eq!(
                read_line(&mut server).await,
                encode_line(&expected_rev_proplist)
            );
            write_item_line(&mut server, &auth_request("realm-1")).await;
            write_item_line(
                &mut server,
                &SvnItem::List(vec![
                    SvnItem::Word("success".to_string()),
                    SvnItem::List(vec![SvnItem::List(vec![SvnItem::List(vec![
                        SvnItem::String(b"p".to_vec()),
                        SvnItem::String(b"v".to_vec()),
                    ])])]),
                ]),
            )
            .await;

            assert_eq!(
                read_line(&mut server).await,
                encode_line(&expected_rev_prop)
            );
            write_item_line(&mut server, &auth_request("realm-2")).await;
            write_item_line(
                &mut server,
                &SvnItem::List(vec![
                    SvnItem::Word("success".to_string()),
                    SvnItem::List(vec![SvnItem::List(vec![SvnItem::String(
                        b"hello".to_vec(),
                    )])]),
                ]),
            )
            .await;

            assert_eq!(
                read_line(&mut server).await,
                encode_line(&expected_rev_prop)
            );
            write_item_line(&mut server, &auth_request("realm-3")).await;
            write_item_line(
                &mut server,
                &SvnItem::List(vec![
                    SvnItem::Word("success".to_string()),
                    SvnItem::List(vec![SvnItem::List(Vec::new())]),
                ]),
            )
            .await;
        });

        let props = session.rev_proplist(5).await.unwrap();
        assert_eq!(props.get("p").unwrap().as_slice(), b"v");

        let value = session.rev_prop(5, "svn:log").await.unwrap();
        assert_eq!(value.as_deref(), Some(b"hello".as_slice()));

        let value = session.rev_prop(5, "svn:log").await.unwrap();
        assert_eq!(value, None);

        server_task.await.unwrap();
    });
}

#[test]
fn check_path_sends_command_and_parses_kind() {
    run_async(async {
        let (mut session, mut server) = connected_session().await;

        let expected_check_path = SvnItem::List(vec![
            SvnItem::Word("check-path".to_string()),
            SvnItem::List(vec![
                SvnItem::String(b"trunk/file.txt".to_vec()),
                SvnItem::List(vec![SvnItem::Number(2)]),
            ]),
        ]);

        let server_task = tokio::spawn(async move {
            assert_eq!(
                read_line(&mut server).await,
                encode_line(&expected_check_path)
            );
            write_item_line(&mut server, &auth_request("realm")).await;
            write_item_line(
                &mut server,
                &SvnItem::List(vec![
                    SvnItem::Word("success".to_string()),
                    SvnItem::List(vec![SvnItem::Word("file".to_string())]),
                ]),
            )
            .await;
        });

        let kind = session.check_path("trunk/file.txt", Some(2)).await.unwrap();
        assert_eq!(kind, NodeKind::File);

        server_task.await.unwrap();
    });
}

#[test]
fn reparent_sends_command_and_updates_base_url() {
    run_async(async {
        let (mut session, mut server) = connected_session().await;

        let new_url = SvnUrl::parse("svn://example.com/repo/branch").unwrap();
        let expected_reparent = SvnItem::List(vec![
            SvnItem::Word("reparent".to_string()),
            SvnItem::List(vec![SvnItem::String(new_url.url.as_bytes().to_vec())]),
        ]);

        let cmd_success = SvnItem::List(vec![
            SvnItem::Word("success".to_string()),
            SvnItem::List(Vec::new()),
        ]);

        let server_task = tokio::spawn(async move {
            assert_eq!(
                read_line(&mut server).await,
                encode_line(&expected_reparent)
            );
            write_item_line(&mut server, &auth_request("realm")).await;
            write_item_line(&mut server, &cmd_success).await;
        });

        session.reparent(new_url.clone()).await.unwrap();
        assert_eq!(session.client.base_url, new_url);

        server_task.await.unwrap();
    });
}

#[test]
fn change_rev_prop_encodes_optional_value() {
    run_async(async {
        let (mut session, mut server) = connected_session().await;

        let expected_with_value = SvnItem::List(vec![
            SvnItem::Word("change-rev-prop".to_string()),
            SvnItem::List(vec![
                SvnItem::Number(9),
                SvnItem::String(b"svn:log".to_vec()),
                SvnItem::String(b"msg".to_vec()),
            ]),
        ]);

        let expected_without_value = SvnItem::List(vec![
            SvnItem::Word("change-rev-prop".to_string()),
            SvnItem::List(vec![
                SvnItem::Number(9),
                SvnItem::String(b"svn:log".to_vec()),
            ]),
        ]);

        let cmd_success = SvnItem::List(vec![
            SvnItem::Word("success".to_string()),
            SvnItem::List(Vec::new()),
        ]);

        let server_task = tokio::spawn(async move {
            assert_eq!(
                read_line(&mut server).await,
                encode_line(&expected_with_value)
            );
            write_item_line(&mut server, &auth_request("realm-1")).await;
            write_item_line(&mut server, &cmd_success).await;

            assert_eq!(
                read_line(&mut server).await,
                encode_line(&expected_without_value)
            );
            write_item_line(&mut server, &auth_request("realm-2")).await;
            write_item_line(&mut server, &cmd_success).await;
        });

        session
            .change_rev_prop(9, "svn:log", Some(b"msg".to_vec()))
            .await
            .unwrap();
        session.change_rev_prop(9, "svn:log", None).await.unwrap();

        server_task.await.unwrap();
    });
}

#[test]
fn change_rev_prop2_encodes_value_tuple_and_conditional() {
    run_async(async {
        let (mut session, mut server) = connected_session().await;

        let expected = SvnItem::List(vec![
            SvnItem::Word("change-rev-prop2".to_string()),
            SvnItem::List(vec![
                SvnItem::Number(9),
                SvnItem::String(b"svn:log".to_vec()),
                SvnItem::List(vec![SvnItem::String(b"new".to_vec())]),
                SvnItem::List(vec![SvnItem::Bool(false), SvnItem::String(b"old".to_vec())]),
            ]),
        ]);

        let cmd_success = SvnItem::List(vec![
            SvnItem::Word("success".to_string()),
            SvnItem::List(Vec::new()),
        ]);

        let server_task = tokio::spawn(async move {
            assert_eq!(read_line(&mut server).await, encode_line(&expected));
            write_item_line(&mut server, &auth_request("realm")).await;
            write_item_line(&mut server, &cmd_success).await;
        });

        session
            .change_rev_prop2(
                9,
                "svn:log",
                Some(b"new".to_vec()),
                false,
                Some(b"old".to_vec()),
            )
            .await
            .unwrap();

        server_task.await.unwrap();
    });
}