The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl -w
# @(#) $Id: XML-Genx.t 1266 2006-10-08 16:26:55Z dom $

use strict;
use warnings;

use File::Temp qw( tempfile );
use Test::More tests => 115;

BEGIN {
    use_ok( 'XML::Genx' );
    use_ok(
        'XML::Genx::Constants', qw(
            GENX_SUCCESS
            GENX_SEQUENCE_ERROR
            GENX_BAD_NAME
            GENX_NON_XML_CHARACTER
            )
    );
}

my $w = XML::Genx->new();
isa_ok( $w, 'XML::Genx' );
can_ok( $w, qw(
    GetVersion
    StartDocFile
    StartDocSender
    LastErrorMessage
    LastErrorCode
    GetErrorMessage
    StartElementLiteral
    AddAttributeLiteral
    EndElement
    EndDocument
    Comment
    PI
    DeclareNamespace
    DeclareElement
    DeclareAttribute
) );

# Subtly different to VERSION()...
is( XML::Genx->GetVersion, 'beta5', 'GetVersion()' );

is(
    test_basics(),
    '<!--hello world-->
<?ping pong?>
<g1:foo xmlns:g1="urn:foo" g1:baz="quux">bar!</g1:foo>',
    'test_basics() output'
);

is(
    test_empty_namespace(),
    '<foo bar="baz"></foo>',
    'test_empty_namespace() output',
);

is(
    test_undef_namespace(),
    '<foo bar="baz"></foo>',
    'test_undef_namespace() output',
);

is(
    test_no_namespace(),
    '<foo bar="baz"></foo>',
    'test_no_namespace() output',
);

test_bad_filehandle();
test_declare_namespace();
test_declare_element();
test_declare_attribute();

is(
    test_declared_in_use(),
    '<foo:bar xmlns:foo="urn:foo" foo:baz="quux"></foo:bar>',
    'test_declared_in_use() output',
);

is(
    test_declared_no_namespace(),
    '<bar baz="quux"></bar>',
    'test_declared_no_namespace() output',
);

is(
    test_declared_with_namespace(),
    '<el xmlns="http://example.com/#ns" xmlns:g1="http://example.com/#ns2" g1:at="val"></el>',
    'test_declared_with_namespace() output',
);

is(
    test_sender(),
    "<foo>\x{0100}dam</foo>",
    'test_sender() output',
);

is(
    test_astral(),
    "<monogram-for-earth>\x{1D300}</monogram-for-earth>",
    'test_astral() output',
);

is(
    test_declared_namespace_in_literal(),
    '<x:foo xmlns:x="urn:foo" x:attr=""></x:foo>',
    'test_declared_namespace_in_literal() output',
);

# One of the examples from the XML canonicalization spec.
is(
    test_c14n_example_3_3(),
    q{<doc>
   <e1></e1>
   <e2></e2>
   <e3 id="elem3" name="elem3"></e3>
   <e4 id="elem4" name="elem4"></e4>
   <e5 xmlns="http://example.org" xmlns:a="http://www.w3.org" xmlns:b="http://www.ietf.org" attr="I'm" attr2="all" b:attr="sorted" a:attr="out"></e5>
   <e6 xmlns:a="http://www.w3.org">
      <e7 xmlns="http://www.ietf.org">
         <e8 xmlns="">
            <e9 xmlns:a="http://www.ietf.org" attr="default"></e9>
         </e8>
      </e7>
   </e6>
</doc>},
    'test_c14n_example_3_3() output'
);

test_die_on_error();
test_constants();
test_fh_scope();
test_scrubtext();

sub test_basics {
    my $w = XML::Genx->new();
    my $fh = tempfile();
    is( $w->StartDocFile( $fh ),  0,         'StartDocFile(fh)' );
    is( $w->LastErrorMessage,     'Success', 'LastErrorMessage()' );
    is( $w->GetErrorMessage( 0 ), 'Success', 'GetErrorMessage(0)' );

    is( $w->Comment( 'hello world' ), 0, 'Comment(hello world)' );
    is( $w->PI( qw( ping pong ) ), 0, 'PI(ping pong)' );
    is( $w->StartElementLiteral( 'urn:foo', 'foo' ),
        0, 'StartElementLiteral(urn:foo,foo)' );
    is( $w->AddAttributeLiteral( 'urn:foo', 'baz', 'quux' ),
        0, 'AddAttributeLiteral(urn:foo,baz,quux)' );
    is( $w->AddText( 'bar' ), 0, 'AddText(bar)' );
    is( $w->AddCharacter( ord( "!" ) ), 0, 'AddCharacter(ord(!))' );
    is( $w->EndElement,       0, 'EndElement()' );
    is( $w->EndDocument,      0, 'EndDocument()' );
    return fh_contents( $fh );
}

sub test_empty_namespace {
    my $w  = XML::Genx->new();
    my $fh = tempfile();
    is( $w->StartDocFile( $fh ), 0, 'StartDocFile(fh)' );
    is(
        $w->StartElementLiteral( '', 'foo' ), 0,
        'StartElementLiteral("",foo)'
    );
    is(
        $w->AddAttributeLiteral( '', bar => 'baz' ), 0,
        'AddAttributeLiteral()'
    );
    is( $w->EndElement,  0, 'EndElement()' );
    is( $w->EndDocument, 0, 'EndDocument()' );
    return fh_contents( $fh );
}

sub test_undef_namespace {
    my $w  = XML::Genx->new();
    my $fh = tempfile();
    is( $w->StartDocFile( $fh ), 0, 'StartDocFile(fh)' );
    is(
        $w->StartElementLiteral( undef, 'foo' ), 0,
        'StartElementLiteral(undef,foo)'
    );
    is(
        $w->AddAttributeLiteral( undef, bar => 'baz' ), 0,
        'AddAttributeLiteral()'
    );
    is( $w->EndElement,  0, 'EndElement()' );
    is( $w->EndDocument, 0, 'EndDocument()' );
    return fh_contents( $fh );
}

sub test_no_namespace {
    my $w  = XML::Genx->new();
    my $fh = tempfile();
    is( $w->StartDocFile( $fh ), 0, 'StartDocFile(fh)' );
    is( $w->StartElementLiteral( 'foo' ), 0, 'StartElementLiteral(foo)' );
    is( $w->AddAttributeLiteral( bar => 'baz' ), 0, 'AddAttributeLiteral()' );
    is( $w->EndElement,  0, 'EndElement()' );
    is( $w->EndDocument, 0, 'EndDocument()' );
    return fh_contents( $fh );
}

sub test_bad_filehandle {
  SKIP: {
        skip 'Need perl 5.8 for in memory file handles.', 1
          if $] < 5.008;

        my $txt = '';
        open( my $fh, '>', \$txt ) or die "open(>\$txt): $!\n";
        my $w = XML::Genx->new;
        eval { $w->StartDocFile( $fh ) };
        like( $@, qr/Bad filehandle/i, 'StartDocFile(bad filehandle)' );
    }
}

sub test_declare_namespace {
    my $w = XML::Genx->new();
    my $ns = $w->DeclareNamespace( 'urn:foo', 'foo' );
    is( $w->LastErrorMessage, 'Success', 'DeclareNamespace()' );
    isa_ok( $ns, 'XML::Genx::Namespace' );
    can_ok( $ns, qw( GetNamespacePrefix AddNamespace ) );
    # This will return undef until we've actually written some XML...
    is( $ns->GetNamespacePrefix, undef, 'GetNamespacePrefix()' );
}

sub test_declare_element {
    my $w = XML::Genx->new();
    my $ns = $w->DeclareNamespace( 'urn:foo', 'foo' );
    my $el = $w->DeclareElement( $ns, 'wibble' );
    is( $w->LastErrorMessage, 'Success', 'DeclareElement()' );
    isa_ok( $el, 'XML::Genx::Element' );
    can_ok( $el, qw( StartElement ) );

    my $el2 = $w->DeclareElement( 'wobble' );
    isa_ok( $el2, 'XML::Genx::Element' );
}

sub test_declare_attribute {
    my $w = XML::Genx->new();
    my $ns = $w->DeclareNamespace( 'urn:foo', 'foo' );
    my $at = $w->DeclareAttribute( $ns, 'wobble' );
    is( $w->LastErrorMessage, 'Success', 'DeclareAttribute()' );
    isa_ok( $at, 'XML::Genx::Attribute' );
    can_ok( $at, qw( AddAttribute ) );

    my $at2 = $w->DeclareAttribute( 'weebl' );
    isa_ok( $at2, 'XML::Genx::Attribute' );
}

sub test_declared_in_use {
    my $w = XML::Genx->new();
    my $ns = $w->DeclareNamespace( 'urn:foo', 'foo' );
    my $el = $w->DeclareElement( $ns, 'bar' );
    my $at = $w->DeclareAttribute( $ns, 'baz' );
    my $fh = tempfile();

    is( $w->StartDocFile( $fh ), 0, 'StartDocFile()' );
    is( $el->StartElement(), 0, 'StartElement()' );
    is( $at->AddAttribute( 'quux' ), 0, 'AddAttribute()' );
    is( $w->EndElement(), 0, 'EndElement()' );
    is( $w->EndDocument(), 0, 'EndDocument()' );
    return fh_contents( $fh );
}

sub test_declared_no_namespace {
    my $w = XML::Genx->new();
    my $el = $w->DeclareElement( undef, 'bar' );
    my $at = $w->DeclareAttribute( undef, 'baz' );
    my $fh = tempfile();

    is( $w->StartDocFile( $fh ), 0, 'StartDocFile()' );
    is( $el->StartElement(), 0, 'StartElement()' );
    is( $at->AddAttribute( 'quux' ), 0, 'AddAttribute()' );
    is( $w->EndElement(), 0, 'EndElement()' );
    is( $w->EndDocument(), 0, 'EndDocument()' );
    return fh_contents( $fh );
}

sub test_declared_with_namespace {
    my $w = XML::Genx->new();

    # Default prefix for this namespace is "foo".
    my $nsurl = 'http://example.com/#ns';
    my $ns    = $w->DeclareNamespace( $nsurl, 'foo' );

    # Ask genx to generate a default prefix here.
    my $ns2url = 'http://example.com/#ns2';
    my $ns2    = $w->DeclareNamespace( $ns2url );

    my $fh = tempfile();
    is( $w->StartDocFile( $fh ), 0, 'StartDocFile()' );
    is( $w->StartElementLiteral( $nsurl, 'el' ), 0, 'StartElement(el)' );

    # Override and attempt to make it the default namespace.
    is( $ns->AddNamespace( '' ), 0, 'AddNamespace("")' )
        or diag $w->LastErrorMessage;

    # Let it keep whatever prefix genx allocated.
    is( $ns2->AddNamespace(), 0, 'AddNamespace()' )
        or diag $w->LastErrorMessage;
    is(
        $w->AddAttributeLiteral( $ns2url, at => 'val' ), 0,
        'AddAttributeLiteral(ns2url,at,val)'
    );
    is( $w->EndElement(),  0, 'EndElement()' );
    is( $w->EndDocument(), 0, 'EndDocument()' );
    return fh_contents( $fh );
}

sub test_sender {
    my $out = '';
    my $w   = XML::Genx->new;
    is( $w->StartDocSender( sub { $out .= $_[0] } ), 0, 'StartDocSender()' );
    is(
        $w->StartElementLiteral( undef, 'foo' ), 0,
        'StartElementLiteral(undef,foo)'
    );
    is( $w->AddText( "\x{0100}dam" ), 0, 'AddText(*utf8*)' );
    is( $w->EndElement,  0, 'EndElement()' );
    is( $w->EndDocument, 0, 'EndDocument()' );
    return $out;
}

sub test_astral {
    my $w  = XML::Genx->new;
    my $fh = tempfile();

    is( $w->StartDocFile( $fh ), 0, 'StartDocFile()' );
    is( $w->StartElementLiteral( undef, 'monogram-for-earth' ),
        0, 'StartElementLiteral(undef,monogram-for-earth)' );
    is( $w->AddText( "\x{1D300}" ), 0, 'AddText(*astral-utf8*)' );
    is( $w->EndElement,             0, 'EndElement()' );
    is( $w->EndDocument,            0, 'EndDocument()' );
    return fh_contents( $fh );
}

sub test_declared_namespace_in_literal {
    my $w  = XML::Genx->new;
    my $fh = tempfile();

    is( $w->StartDocFile( $fh ), 0, 'StartDocFile()' );
    my $ns = $w->DeclareNamespace( "urn:foo", "x" );
    isa_ok( $ns, 'XML::Genx::Namespace' );
    is( $w->StartElementLiteral( $ns, 'foo' ),
        0, 'StartElementLiteral(ns,foo)' );
    is( $w->AddAttributeLiteral( $ns, 'attr', '' ),
        0, 'AddAttributeLiteral(x:attr)' );
    is( $w->EndElement,  0, 'EndElement()' );
    is( $w->EndDocument, 0, 'EndDocument()' );
    return fh_contents( $fh );
}

# Check that start and end tags work according to spec.  This example is
# horrible.  <http://www.w3.org/TR/xml-c14n#Example-SETags>
sub test_c14n_example_3_3 {
    my $fh = tempfile();
    my $w  = XML::Genx->new;
    my $indent = "   ";

    # Attempt to replicate <http://www.w3.org/TR/xml-c14n#Example-SETags>.
    $w->StartDocFile( $fh );
    $w->StartElementLiteral( 'doc' );
    $w->AddText( "\n" . $indent );

    $w->StartElementLiteral( 'e1' );
    $w->EndElement;
    $w->AddText( "\n" . $indent );

    $w->StartElementLiteral( 'e2' );
    $w->EndElement;
    $w->AddText( "\n" . $indent );

    $w->StartElementLiteral( 'e3' );
    $w->AddAttributeLiteral( name => 'elem3' );
    $w->AddAttributeLiteral( id   => 'elem3' );
    $w->EndElement;
    $w->AddText( "\n" . $indent );

    $w->StartElementLiteral( 'e4' );
    $w->AddAttributeLiteral( name => 'elem4' );
    $w->AddAttributeLiteral( id   => 'elem4' );
    $w->EndElement;
    $w->AddText( "\n" . $indent );

    my $ns_a    = $w->DeclareNamespace( 'http://www.w3.org',   'a' );
    my $ns_b    = $w->DeclareNamespace( 'http://www.ietf.org', 'b' );
    my $ns_dflt = $w->DeclareNamespace( 'http://example.org',  '' );

    $w->StartElementLiteral( $ns_dflt, 'e5' );
    $w->AddAttributeLiteral( attr2 => 'all' );
    $w->AddAttributeLiteral( $ns_a, attr => 'out' );
    $w->AddAttributeLiteral( $ns_b, attr => 'sorted' );
    $w->AddAttributeLiteral( attr => "I'm" );
    $w->EndElement;
    $w->AddText( "\n" . $indent );

    $w->StartElementLiteral( 'e6' );
    $ns_a->AddNamespace;
    $w->AddText( "\n" . ($indent x 2) );

    $w->StartElementLiteral( $ns_b, 'e7' );
    $ns_b->AddNamespace( '' );
    $w->AddText( "\n" . ($indent x 3) );

    $w->StartElementLiteral( 'e8' );
    $w->UnsetDefaultNamespace;
    $w->AddText( "\n" . ($indent x 4) );

    $w->StartElementLiteral( 'e9' );
    $ns_b->AddNamespace( 'a' );
    $w->AddAttributeLiteral( attr => 'default' );
    $w->EndElement;    # e9
    $w->AddText( "\n" . ($indent x 3) );

    $w->EndElement;    # e8
    $w->AddText( "\n" . ($indent x 2) );

    $w->EndElement;    # e7
    $w->AddText( "\n" . $indent );

    $w->EndElement;    # e6
    $w->AddText( "\n" );
    $w->EndElement;    # doc
    $w->EndDocument;
    return fh_contents( $fh );
}

sub test_die_on_error {
    my $w = XML::Genx->new;
    cmp_ok( $w->LastErrorCode, '==', 0, 'LastErrorCode() after new()' );
    eval { $w->EndDocument };
    like( $@, qr/^Call out of sequence/, 'EndDocument() sequence error' )
        or diag $@;

    # This is needed because I originally wrote a version that used
    # exception objects where I shouldn't have.  Now that I've switched
    # to plain strings, I expect them to report where they have croaked.
    my $thisfile = __FILE__;
    like( $@, qr/ at \Q$thisfile/, 'Exception reports location.' );

    # This is the new way to determine more exactly what happened.
    cmp_ok( $w->LastErrorCode, '==', GENX_SEQUENCE_ERROR,
        'LastErrorCode() after an exception.' );

    $w = XML::Genx->new;    # Clear error status.
    eval {
        my $ns = $w->DeclareNamespace( 'urn:foo', 'foo' );
        isa_ok( $ns, 'XML::Genx::Namespace' );
        $ns->AddNamespace();
    };
    like( $@, qr/^Call out of sequence/, 'ns->AddNamespace() sequence error' );
    cmp_ok( $w->LastErrorCode, '==', GENX_SEQUENCE_ERROR,
        'LastErrorCode() after an exception.' );

    $w = XML::Genx->new;    # Clear error status.
    eval {
        my $el = $w->DeclareElement( 'foo' );
        isa_ok( $el, 'XML::Genx::Element' );
        $el->StartElement();
    };
    like( $@, qr/^Call out of sequence/, 'el->StartElement() sequence error' );
    cmp_ok( $w->LastErrorCode, '==', GENX_SEQUENCE_ERROR,
        'LastErrorCode() after an exception.' );

    $w = XML::Genx->new;    # Clear error status.
    eval {
        my $at = $w->DeclareAttribute( 'foo' );
        isa_ok( $at, 'XML::Genx::Attribute' );
        $at->AddAttribute( 'bar' );
    };
    like( $@, qr/^Call out of sequence/, 'at->AddAttribute() sequence error' );
    cmp_ok( $w->LastErrorCode, '==', GENX_SEQUENCE_ERROR,
        'LastErrorCode() after an exception.' );

    $w = XML::Genx->new;    # Clear error status.
    eval { $w->StartElementLiteral( "\x01" ) };
    like( $@, qr/^Bad NAME/, 'StartElementLiteral() invalid char');
    cmp_ok( $w->LastErrorCode, '==', GENX_BAD_NAME,
        'LastErrorCode() after an exception.' );

    $w = XML::Genx->new;    # Clear error status.
    eval {
        my $fh = tempfile();
        $w->StartDocFile( $fh );
        $w->StartElementLiteral( "foo" );
        $w->AddAttributeLiteral( "bar" => "\x01" );
    };
    like( $@, qr/^Non XML Character/, 'AddAttributeLiteral() invalid char');
    cmp_ok( $w->LastErrorCode, '==', GENX_NON_XML_CHARACTER,
        'LastErrorCode() after an exception.' );

    $w = XML::Genx->new;    # Clear error status.
    eval {
        my $fh = tempfile();
        $w->StartDocFile( $fh );
        $w->StartElementLiteral( "foo" );
        $w->AddCharacter( 1 );
    };
    like( $@, qr/^Non XML Character/, 'AddCharacter() invalid char');
    cmp_ok( $w->LastErrorCode, '==', GENX_NON_XML_CHARACTER,
        'LastErrorCode() after an exception.' );

}

sub test_constants {
    my $w = XML::Genx->new;
    is( GENX_SUCCESS, 0, 'GENX_SUCCESS' );
    eval { $w->EndDocument };
    cmp_ok( $w->LastErrorCode, '==', GENX_SEQUENCE_ERROR,
        'GENX_SEQUENCE_ERROR' );
}

sub test_fh_scope {
    my $w = XML::Genx->new;
    {
        my $fh = tempfile();
        is( $w->StartDocFile( $fh ), 0, 'StartDocFile()' );
    }
    is( $w->StartElementLiteral( 'foo' ), 0, 'StartElementLiteral(foo)' );
    is( $w->EndElement, 0, 'EndElement()' );
    is( $w->EndDocument, 0, 'EndDocument()' );
    # We don't actually care what's been written at this point.  Just
    # that it *has* been written without blowing up.
    return;
}

sub test_scrubtext {
    my $w = XML::Genx->new();
    is( $w->ScrubText( "abc" ),     "abc", 'ScrubText() all good' );
    is( $w->ScrubText( "abc\x01" ), "abc", 'ScrubText() skips non-xml chars' );
}

sub test_perl_strings {
    my $w  = XML::Genx->new;
    my $fh = tempfile();
    is( $w->StartDocFile( $fh ), 0, 'StartDocFile()' );
    is( $w->StartElementLiteral('foo'), 0, 'StartElementLiteral()');
    is( $w->AddText( do { use bytes; "\xA0" } ), 0, 'AddText(\xA0) as bytes' );
    is( $w->EndElement, 0, 'EndElement()');
    is( $w->EndDocument, 0, 'EndDocument()');
    is( fh_contents($fh), "<foo>\xA0</foo>", 'test_perl_strings');
    return;
}

sub fh_contents {
    my $fh = shift;
    # In perl 5.8+, read proper characters.  I /think/ that perl 5.6
    # tries to autodetect this.
    binmode( $fh, ':utf8' ) if $] >= 5.008;
    seek $fh, 0, 0 or die "seek: $!\n";
    local $/;
    return <$fh>;
}

# vim: set ai et sw=4 syntax=perl :