summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore7
-rw-r--r--Build.PL59
-rw-r--r--MANIFEST15
-rw-r--r--lib/App/DocKnot.pm391
-rw-r--r--share/licenses.json5
-rw-r--r--share/licenses/Expat17
-rw-r--r--share/templates/readme.tmpl48
-rwxr-xr-xt/api/basic.t52
-rw-r--r--t/data/lbcd/metadata/metadata.json28
-rw-r--r--t/data/lbcd/metadata/sections/blurb6
-rw-r--r--t/data/lbcd/metadata/sections/description24
-rw-r--r--t/data/lbcd/metadata/sections/installation30
-rw-r--r--t/data/lbcd/metadata/sections/requirements14
-rw-r--r--t/data/lbcd/metadata/sections/support16
-rw-r--r--t/data/lbcd/metadata/sections/testing13
-rw-r--r--t/data/lbcd/output/readme183
16 files changed, 908 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..44dad35
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+/Build
+/MANIFEST.bak
+/MYMETA.json
+/MYMETA.yml
+/_build/
+/blib/
+/cover_db
diff --git a/Build.PL b/Build.PL
new file mode 100644
index 0000000..aef481a
--- /dev/null
+++ b/Build.PL
@@ -0,0 +1,59 @@
+#!/usr/bin/perl
+#
+# Build script for the docknot application.
+#
+# Written by Russ Allbery <rra@stanford.edu>
+# Copyright 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+use 5.018;
+use autodie;
+use warnings;
+
+use Module::Build;
+
+# Basic package configuration.
+my $build = Module::Build->new(
+ dist_name => 'docknot',
+ dist_abstract => 'Package documentation generator',
+ dist_author => 'Russ Allbery <eagle@eyrie.org>',
+ dist_version => '1.00',
+ license => 'mit',
+ module_name => 'App::DocKnot',
+ recursive_test_files => 1,
+
+ # Module static data files.
+ share_dir => { module => { 'App::DocKnot' => 'share' } },
+
+ # Other package relationships.
+ configure_requires => { 'Module::Build' => 0.36 },
+ requires => {
+ 'File::BaseDir' => 0,
+ 'File::ShareDir' => 0,
+ 'JSON' => 0,
+ 'Perl6::Slurp' => 0,
+ 'Template' => 0,
+ perl => '5.018',
+ },
+);
+
+# Generate the build script.
+$build->create_build_script;
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000..c9c0472
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,15 @@
+Build.PL
+lib/App/DocKnot.pm
+MANIFEST This list of files
+share/licenses.json
+share/licenses/Expat
+share/templates/readme.tmpl
+t/api/basic.t
+t/data/lbcd/metadata/metadata.json
+t/data/lbcd/metadata/sections/blurb
+t/data/lbcd/metadata/sections/description
+t/data/lbcd/metadata/sections/installation
+t/data/lbcd/metadata/sections/requirements
+t/data/lbcd/metadata/sections/support
+t/data/lbcd/metadata/sections/testing
+t/data/lbcd/output/readme
diff --git a/lib/App/DocKnot.pm b/lib/App/DocKnot.pm
new file mode 100644
index 0000000..0d8d3f2
--- /dev/null
+++ b/lib/App/DocKnot.pm
@@ -0,0 +1,391 @@
+# Implementation of the DocKnot application.
+#
+# This is the primary class for the DocKnot application, which supports
+# generation of various documentation files based on package metadata and
+# general templates.
+#
+# Written by Russ Allbery <rra@stanford.edu>
+# Copyright 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+##############################################################################
+# Modules and declarations
+##############################################################################
+
+package App::DocKnot 1.00;
+
+use 5.018;
+use autodie;
+use warnings;
+
+use Carp qw(croak);
+use File::BaseDir qw(config_files);
+use File::ShareDir qw(module_file);
+use File::Spec;
+use JSON;
+use Perl6::Slurp;
+use Template;
+use Text::Wrap qw(wrap);
+
+##############################################################################
+# Generator functions
+##############################################################################
+
+# The internal helper object methods in this section are generators. They
+# return code references intended to be passed into Template Toolkit as code
+# references so that they can be called inside templates, incorporating data
+# from the App::DocKnot configuration or the package metadata.
+
+# Returns code to center a line in $self->{width} characters given the text of
+# the line. The returned code will take a line of text and return that line
+# with leading whitespace added as required.
+#
+# $self - The App::DocKnot object
+#
+# Returns: Code reference to a closure that uses $self->{width} for width
+sub _code_for_center {
+ my ($self) = @_;
+ my $center = sub {
+ my ($text) = @_;
+ my $space = $self->{width} - length($text);
+ if ($space <= 0) {
+ return $text;
+ } else {
+ return q{ } x int($space / 2) . $text;
+ }
+ };
+ return $center;
+}
+
+# Returns code that formats the copyright notices for the package. The
+# resulting code reference takes one parameter, the indentation level, and
+# wraps the copyright notices accordingly. They will be wrapped with a
+# four-space outdent and kept within $self->{width} columns.
+#
+# $self - The App::DocKnot object
+# $copyrights_ref - A reference to a list of anonymous hashes, each with keys:
+# holder - The copyright holder for that copyright
+# years - The years of that copyright
+#
+# Returns: Code reference to a closure taking an indent level and returning
+# the formatted copyright notice
+sub _code_for_copyright {
+ my ($self, $copyrights_ref) = @_;
+ my $copyright = sub {
+ my ($indent) = @_;
+ my $prefix = q{ } x $indent;
+ my $notice;
+ for my $copyright (@{$copyrights_ref}) {
+ my $holder = $copyright->{holder};
+ my $years = $copyright->{years};
+
+ # Build the initial notice with the word copyright and the years.
+ my $text = "Copyright " . $copyright->{years};
+ local $Text::Wrap::columns = $self->{width} + 1;
+ local $Text::Wrap::unexpand = 0;
+ $text = wrap($prefix, $prefix . q{ } x 4, $text);
+
+ # See if the holder fits on the last line. If so, add it there;
+ # otherwise, add another line.
+ my $last_length;
+ if (rindex($text, "\n") == -1) {
+ $last_length = length($text);
+ } else {
+ $last_length = length($text) - rindex($text, "\n");
+ }
+ if ($last_length + length($holder) < $self->{width}) {
+ $text .= " $holder";
+ } else {
+ $text .= "\n" . $prefix . q{ } x 4 . $holder;
+ }
+ $notice .= $text . "\n";
+ }
+ chomp($notice);
+ return $notice;
+ };
+ return $copyright;
+}
+
+# Returns code to indent each line of a paragraph by a given number of spaces.
+# This is constructed as a method returning a closure so that its behavior can
+# be influenced by App::DocKnot configuration in the future, but it currently
+# doesn't use any configuration.
+#
+# $self - The App::DocKnot object
+#
+# Returns: Code reference to a closure
+sub _code_for_indent {
+ my ($self) = @_;
+ my $indent = sub {
+ my ($text, $space) = @_;
+ my @text = split(m{\n}xms, $text);
+ return join("\n", map { q{ } x $space . $_ } @text);
+ };
+ return $indent;
+}
+
+##############################################################################
+# Helper methods
+##############################################################################
+
+# Internal helper routine to return the path of a file from the application
+# data. These data files are installed with App::DocKnot, but each file can
+# be overridden by the user via files in $HOME/.config/docknot or
+# /etc/xdg/docknot (or whatever $XDG_CONFIG_DIRS is set to).
+#
+# We therefore try File::BaseDir first (which handles the XDG paths) and fall
+# back on using File::ShareDir to locate the data.
+#
+# $self - The App::DocKnot object
+# @path - The relative path of the file as a list of components
+#
+# Returns: The absolute path to the application data
+# Throws: Text exception on failure to locate the desired file
+sub _appdata_path {
+ my ($self, @path) = @_;
+
+ # Try XDG paths first.
+ my $path = config_files(@path);
+
+ # If that doesn't work, use the data that came with the module.
+ if (!defined($path)) {
+ $path = module_file('App::DocKnot', File::Spec->catfile(@path));
+ }
+ return $path;
+}
+
+# Internal helper routine that locates an application data file, interprets it
+# as JSON, and returns the resulting decoded contents. This uses the relaxed
+# parsing mode, so comments and commas after data elements are supported.
+#
+# $self - The App::DocKnot object
+# @path - The path of the file to load, as a list of components
+#
+# Returns: Anonymous hash or array resulting from decoding the JSON object
+# Throws: slurp or JSON exception on failure to load or decode the object
+sub _load_appdata_json {
+ my ($self, @path) = @_;
+ my $path = $self->_appdata_path(@path);
+ my $json = JSON->new;
+ $json->relaxed;
+ return $json->decode(scalar(slurp($path)));
+}
+
+# Internal helper routine to return the path of a file or directory from the
+# package metadata directory. The resulting file or directory path is not
+# checked for existence.
+#
+# $self - The App::DocKnot object
+# @path - The relative path of the file as a list of components
+#
+# Returns: The absolute path in the metadata directory
+sub _metadata_path {
+ my ($self, @path) = @_;
+ return File::Spec->catdir($self->{metadata}, @path);
+}
+
+# Internal helper routine to read a file from the package metadata directory
+# and return the contents. The file is specified as a list of path
+# components.
+#
+# $self - The App::DocKnot object
+# @path - The path of the file to load, as a list of components
+#
+# Returns: The contents of the file as a string
+# Throws: slurp exception on failure to read the file
+sub _load_metadata {
+ my ($self, @path) = @_;
+ return slurp($self->_metadata_path(@path));
+}
+
+# Like _load_metadata, but interprets the contents of the metadata file as
+# JSON and decodes it, returning the resulting object. This uses the relaxed
+# parsing mode, so comments and commas after data elements are supported.
+#
+# $self - The App::DocKnot object
+# @path - The path of the file to load, as a list of components
+#
+# Returns: Anonymous hash or array resulting from decoding the JSON object
+# Throws: slurp or JSON exception on failure to load or decode the object
+sub _load_metadata_json {
+ my ($self, @path) = @_;
+ my $data = $self->_load_metadata(@path);
+ my $json = JSON->new;
+ $json->relaxed;
+ return $json->decode($data);
+}
+
+##############################################################################
+# Public interface
+##############################################################################
+
+# Create a new App::DocKnot object, which will be used for subsequent calls.
+#
+# $class - Class of object to create
+# $args - Anonymous hash of arguments with the following keys:
+# width - Line length at which to wrap output files
+# metadata - Path to the directory containing package metadata
+#
+# Returns: Newly created object
+# Throws: Text exceptions on invalid metadata directory path
+sub new {
+ my ($class, $args_ref) = @_;
+
+ # Ensure we were given a valid metadata argument.
+ my $metadata = $args_ref->{metadata};
+ if (!defined($metadata)) {
+ croak('Missing metadata argument to new');
+ }
+ if (!-d $metadata) {
+ croak("Metadata path $metadata does not exist or is not a directory");
+ }
+
+ # Create and return the object.
+ my $self = {
+ metadata => $metadata,
+ width => $args_ref->{width} // 74,
+ };
+ bless($self, $class);
+ return $self;
+}
+
+# Generate a documentation file from the package metadata. Takes the template
+# to use and returns the generated documentation.
+#
+# $self - The App::DocKnot object
+# $template - Name of the documentation template (using Template Toolkit)
+#
+# Returns: The generated documentation as a string
+# Throws: autodie exception on failure to read metadata or write the output
+# Text exception on Template Toolkit failures
+# Text exception on inconsistencies in the package data
+sub generate {
+ my ($self, $template) = @_;
+
+ # Load the package metadata from JSON.
+ my $data_ref = $self->_load_metadata_json('metadata.json');
+
+ # Load documentation sections. readme.sections will contain a list of
+ # sections to add to the README file.
+ for my $section (@{ $data_ref->{readme}{sections} }) {
+ my $title = $section->{title};
+
+ # The file containing the section data will match the title, converted
+ # to lowercase and with spaces changed to dashes.
+ my $file = lc($title);
+ $file =~ s{ [ ] }{-}xms;
+
+ # Load the section content.
+ $section->{body} = $self->_load_metadata('sections', $file);
+ }
+
+ # Expand the package license into license text.
+ my $license = $data_ref->{license};
+ my $licenses_ref = $self->_load_appdata_json('licenses.json');
+ if (!exists($licenses_ref->{$license})) {
+ die "Unknown license $license";
+ }
+ my $license_text = slurp($self->_appdata_path('licenses', $license));
+ $data_ref->{license} = {
+ %{ $licenses_ref->{$license} },
+ full => $license_text,
+ };
+
+ # Create the variable information for the template. Start with all
+ # metadata as loaded above.
+ my %vars = %{$data_ref};
+
+ # Add code references for our defined helper functions.
+ $vars{center} = $self->_code_for_center;
+ $vars{copyright} = $self->_code_for_copyright($data_ref->{copyrights});
+ $vars{indent} = $self->_code_for_indent;
+
+ # Find the path to the relevant template.
+ $template = $self->_appdata_path('templates', "${template}.tmpl");
+
+ # Run Template Toolkit processing.
+ my $tt = Template->new({ ABSOLUTE => 1 }) or die Template->error;
+ my $result;
+ $tt->process($template, \%vars, \$result) or die $tt->error;
+
+ # Word-wrap the results to our width. This requires some annoying
+ # heuristics, but the alternative is to try to get the template to always
+ # produce correctly-wrapped results, which is far harder.
+ #
+ # First, break the resulting text up into paragraphs. (This will also
+ # turn more than two consecutive newlines into just two newlines.)
+ my @paragraphs = split(m{ \n(?:[ ]*\n)+ }xms, $result);
+
+ # Add back the trailing newlines at the end of each paragraph.
+ @paragraphs = map { $_ . "\n" } @paragraphs;
+
+ # For each paragraph that looks like regular text, which means indented by
+ # two or four spaces and consistently on each line, remove the indentation
+ # and then add it back in while wrapping the text.
+ for my $paragraph (@paragraphs) {
+ my ($indent) = ($paragraph =~ m{ \A ([ ]+) \S }xms);
+
+ # If the paragraph is not indented, skip it.
+ next if !defined($indent);
+
+ # If the indent is longer than four characters, leave it alone.
+ next if length($indent) > 4;
+
+ # If this looks like a bullet list, leave it alone.
+ next if $paragraph =~ m{ \A \s+ [*] }xms;
+
+ # If this paragraph is not consistently indented, leave it alone.
+ next if $paragraph !~ m{ \A (?: \Q$indent\E \S[^\n]+ \n )+ \z }xms;
+
+ # Strip the indent from each line.
+ $paragraph =~ s{ (?: \A | (?<=\n) ) \Q$indent\E }{}xmsg;
+
+ # Remove any existing newlines, preserving two spaces after periods.
+ $paragraph =~ s{ [.] \n (\S) }{. $1}xmsg;
+ $paragraph =~ s{ \n(\S) }{ $1}xmsg;
+
+ # Force locally correct configuration of Text::Wrap.
+ local $Text::Wrap::break = qr{\s+}xms;
+ local $Text::Wrap::columns = $self->{width} + 1;
+ local $Text::Wrap::unexpand = 0;
+
+ # Do the wrapping. This modifies @paragraphs in place.
+ $paragraph = wrap($indent, $indent, $paragraph);
+
+ # Strip any trailing whitespace, since some gets left behind after
+ # periods by Text::Wrap.
+ $paragraph =~ s{ [ ]+ \n }{\n}xmsg;
+ }
+
+ # Glue the paragraphs back together and return the result. Because the
+ # last newline won't get stripped by the split above, we have to strip an
+ # extra newline from the end of the file.
+ $result = join("\n", @paragraphs);
+ $result =~ s{ \n+ \z }{\n}xms;
+ return $result;
+}
+
+##############################################################################
+# Module return value and documentation
+##############################################################################
+
+1;
+__END__
diff --git a/share/licenses.json b/share/licenses.json
new file mode 100644
index 0000000..23fd756
--- /dev/null
+++ b/share/licenses.json
@@ -0,0 +1,5 @@
+{
+ "Expat": {
+ "summary": "BSD-style license",
+ },
+}
diff --git a/share/licenses/Expat b/share/licenses/Expat
new file mode 100644
index 0000000..1e4a5f8
--- /dev/null
+++ b/share/licenses/Expat
@@ -0,0 +1,17 @@
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/share/templates/readme.tmpl b/share/templates/readme.tmpl
new file mode 100644
index 0000000..d9f5ffa
--- /dev/null
+++ b/share/templates/readme.tmpl
@@ -0,0 +1,48 @@
+[% center("$name version $version") %]
+[% center("($synopsis)") %]
+[% center("Maintained by $maintainer") %]
+
+ [% FOREACH copr IN copyrights %]Copyright [% copr.years %]
+ [% copr.holder %]. [% END %]This software is distributed under a
+ [% license.summary %]. Please see the section LICENSE below for more
+ information.
+
+[% FOREACH section IN readme.sections %][% section.title %]
+
+[% indent(section.body, 2) %]
+
+[% END %]SOURCE REPOSITORY
+
+ [% name %] is maintained using [% vcs.type %]. You can access the
+ current source by cloning the repository at:
+
+ [% vcs.url %]
+
+ or view the repository via the web at:
+
+ [% vcs.browse %]
+[% IF vcs.type == 'Git' %]
+ When contributing modifications, patches (possibly generated by
+ git-format-patch) are preferred to Git pull requests.
+[% END %]
+LICENSE
+
+ The [% name %] package as a whole is covered by the following copyright
+ statement and license:
+
+[% copyright(4) %]
+
+[% indent(license.full, 4) %]
+
+ All individual files without an explicit exception below are released
+ under this license. Some files may have additional copyright holders as
+ noted in those files. There is detailed information about the licensing
+ of each file in the LICENSE file in this distribution.
+
+ Some files in this distribution are individually released under
+ different licenses, all of which are compatible with the above general
+ package license but which may require preservation of additional
+ notices. All required notices are preserved in the LICENSE file.
+
+ For any copyright range specified by files in this package as YYYY-ZZZZ,
+ the range specifies every single year in that closed interval.
diff --git a/t/api/basic.t b/t/api/basic.t
new file mode 100755
index 0000000..7b93898
--- /dev/null
+++ b/t/api/basic.t
@@ -0,0 +1,52 @@
+#!/usr/bin/perl
+#
+# Tests for the App::DocKnot module API.
+#
+# Written by Russ Allbery <rra@stanford.edu>
+# Copyright 2013
+# The Board of Trustees of the Leland Stanford Junior University
+#
+# See LICENSE for licensing terms.
+
+use 5.018;
+use autodie;
+use warnings;
+
+use File::Spec;
+use IPC::System::Simple qw(capturex);
+use Perl6::Slurp;
+use Test::More tests => 3;
+
+# Load the module.
+BEGIN { use_ok('App::DocKnot') }
+
+# We have a set of test cases in the data directory. Each of them contains
+# metadata and output directories.
+my $dataroot = File::Spec->catfile('t', 'data');
+opendir(my $tests, File::Spec->catfile('t', 'data'));
+my @tests = File::Spec->no_upwards(readdir($tests));
+closedir($tests);
+@tests = grep { -d File::Spec->catfile($dataroot, $_, 'metadata') } @tests;
+
+# For each of those cases, initialize an object from the metadata directory,
+# generate a file from the readme template, and compare that with the readme
+# in the output directory.
+for my $test (@tests) {
+ my $path = File::Spec->catfile($dataroot, $test, 'metadata');
+ my $docknot = App::DocKnot->new({ metadata => $path });
+ isa_ok($docknot, 'App::DocKnot', "for $test");
+ my $got = $docknot->generate('readme');
+ $path = File::Spec->catfile($dataroot, $test, 'output', 'readme');
+ my $expected = slurp($path);
+ if ($got eq $expected) {
+ is($got, $expected, "README for $test");
+ } else {
+ open(my $tmp, q{>}, 'tmp');
+ print {$tmp} $got or die "Cannot write to tmp: $!\n";
+ close($tmp);
+ my $diff = '# ' . capturex([0..1], 'diff', '-u', $path, 'tmp');
+ diag($diff);
+ unlink('tmp');
+ ok(0, "README for $test");
+ }
+}
diff --git a/t/data/lbcd/metadata/metadata.json b/t/data/lbcd/metadata/metadata.json
new file mode 100644
index 0000000..4d872cc
--- /dev/null
+++ b/t/data/lbcd/metadata/metadata.json
@@ -0,0 +1,28 @@
+{
+ "name": "lbcd",
+ "version": "3.4.2",
+ "synopsis": "responder for load balancing",
+ "maintainer": "Russ Allbery <eagle@eyrie.org>",
+ "copyrights": [
+ {
+ "holder": "The Board of Trustees of the Leland Stanford Junior University",
+ "years": "1993-1994, 1996-1998, 2000, 2003-2009, 2012-2013",
+ },
+ ],
+ "license": "Expat",
+ "vcs": {
+ "type": "Git",
+ "url": "git://git.eyrie.org/system/lbcd.git",
+ "browse": "http://git.eyrie.org/?p=system/lbcd.git",
+ },
+ "readme": {
+ "sections": [
+ { "title": "BLURB" },
+ { "title": "DESCRIPTION" },
+ { "title": "REQUIREMENTS" },
+ { "title": "INSTALLATION" },
+ { "title": "TESTING" },
+ { "title": "SUPPORT" },
+ ],
+ },
+}
diff --git a/t/data/lbcd/metadata/sections/blurb b/t/data/lbcd/metadata/sections/blurb
new file mode 100644
index 0000000..3689eb9
--- /dev/null
+++ b/t/data/lbcd/metadata/sections/blurb
@@ -0,0 +1,6 @@
+lbcd is a daemon that runs on a UNIX system and answers UDP queries with
+information about system load, number of logged-on users, uptime, and free
+/tmp space. This information can be used to accumulate system status
+across a cluster with light-weight queries or can be used as input to a
+load-balancing system to choose the best system to which to direct new
+incoming connections.
diff --git a/t/data/lbcd/metadata/sections/description b/t/data/lbcd/metadata/sections/description
new file mode 100644
index 0000000..f634859
--- /dev/null
+++ b/t/data/lbcd/metadata/sections/description
@@ -0,0 +1,24 @@
+The lbcd daemon runs on a UNIX system and answers UDP queries (by default
+on port 4330) with information about system load, number of logged on
+users, uptime, and free /tmp space. This information can be used to
+accumulate system status across a cluster with light-weight queries or can
+be used as input to a load-balancing system to choose the best system to
+which to direct new incoming connections. It was designed for use with
+the lbnamed DNS load balancer, available at:
+
+ <http://www.stanford.edu/~riepel/lbnamed/>
+
+It was originally written by Roland Schemers, was rewritten by Larry
+Schwimmer to add protocol version 3 with some additional features and
+service probing, and is currently maintained by Russ Allbery.
+
+The information provided isn't particularly sophisticated, and a good
+hardware load balancer will be able to consider such things as connection
+latency and responsiveness to make better decisions, but lbcd with lbnamed
+works quite well for smaller scale problems, scales well to multiple load
+balance pools for different services, and is much simpler and cheaper to
+understand and deploy.
+
+Included in this package is a small client program, lbcdclient, which can
+query an lbcd server and display a formatted version of the returned
+information.
diff --git a/t/data/lbcd/metadata/sections/installation b/t/data/lbcd/metadata/sections/installation
new file mode 100644
index 0000000..bf56543
--- /dev/null
+++ b/t/data/lbcd/metadata/sections/installation
@@ -0,0 +1,30 @@
+You can build and install remctl with the standard commands:
+
+ ./configure
+ make
+ make install
+
+Pass --enable-silent-rules to configure for a quieter build (similar to
+the Linux kernel). Use make warnings instead of make to build with full
+GCC compiler warnings (requires a relatively current version of GCC).
+
+The last step will probably have to be done as root. By default, remctl
+installs itself under /usr/local; you can change that path by passing the
+--prefix=PATH argument to configure.
+
+lbcd looks for $sysconfdir/nolbcd and returns the maximum load if that
+file is present, allowing one to effectively drop a system out of a
+load-balanced pool by touching that file. By default, the path is
+/usr/local/etc/nolbcd, but you may want to pass --sysconfdir=/etc to
+configure to use /etc/nolbcd.
+
+lbcdclient is written in Perl, so you may have to edit the first line of
+the script to point to the correct Perl location on your system. It does
+not use any sophisticated Perl features or add-on modules.
+
+You will generally want to start lbcd at system boot. All that is needed
+is a simple init script to start lbcd with the appropriate options or kill
+it again. It writes its PID into /var/run/lbcd.pid by default (and this
+can be changed with the -P option). On many systems, lbcd will need to
+run as root or as a member of particular groups to obtain system load
+average and uptime information.
diff --git a/t/data/lbcd/metadata/sections/requirements b/t/data/lbcd/metadata/sections/requirements
new file mode 100644
index 0000000..c949b73
--- /dev/null
+++ b/t/data/lbcd/metadata/sections/requirements
@@ -0,0 +1,14 @@
+lbcd is written in C, so you'll need a C compiler. It also uses kernel
+calls to obtain load and uptime information, and at present has only been
+ported to Linux, Solaris, AIX, various BSD systems, Mac OS X, HP-UX, IRIX,
+and Tru64. It is currently primarily tested on Linux. Platforms not
+listed may require some porting effort, as may old or unusual platforms
+that aren't regularly tested.
+
+The lbcdclient program requires Perl (any version should do).
+
+To bootstrap from a Git checkout, or if you change the Automake files and
+need to regenerate Makefile.in, you will need Automake 1.11 or later. For
+bootstrap or if you change configure.ac or any of the m4 files it includes
+and need to regenerate configure or config.h.in, you will need Autoconf
+2.64 or later.
diff --git a/t/data/lbcd/metadata/sections/support b/t/data/lbcd/metadata/sections/support
new file mode 100644
index 0000000..3fc91f7
--- /dev/null
+++ b/t/data/lbcd/metadata/sections/support
@@ -0,0 +1,16 @@
+The lbcd web page at:
+
+ http://www.eyrie.org/~eagle/software/lbcd/
+
+will always have the current version of this package, the current
+documentation, and pointers to any additional resources.
+
+New lbcd releases are announced on the low-volume lbnamed-users mailing
+list. To subscribe or see the list archives, go to:
+
+ https://mailman.stanford.edu/mailman/listinfo/lbnamed-users
+
+I welcome bug reports and patches for this package at eagle@eyrie.org.
+However, please be aware that I tend to be extremely busy and work
+projects often take priority. I'll save your mail and get to it as soon
+as I can, but it may take me a couple of months.
diff --git a/t/data/lbcd/metadata/sections/testing b/t/data/lbcd/metadata/sections/testing
new file mode 100644
index 0000000..4b753d9
--- /dev/null
+++ b/t/data/lbcd/metadata/sections/testing
@@ -0,0 +1,13 @@
+lbcd comes with a rudimentary test suite, which you can run after building
+lbcd with:
+
+ make check
+
+If a test case fails, please run the that individual test case with
+verbose output using:
+
+ tests/runtests -o <name-of-test>
+
+and send me the output when reporting the problem. Currently, the test
+suite only checks the portability and utility libraries; tests for lbcd
+and lbcdclient are coming in later releases.
diff --git a/t/data/lbcd/output/readme b/t/data/lbcd/output/readme
new file mode 100644
index 0000000..3ff1c88
--- /dev/null
+++ b/t/data/lbcd/output/readme
@@ -0,0 +1,183 @@
+ lbcd version 3.4.2
+ (responder for load balancing)
+ Maintained by Russ Allbery <eagle@eyrie.org>
+
+ Copyright 1993-1994, 1996-1998, 2000, 2003-2009, 2012-2013 The Board of
+ Trustees of the Leland Stanford Junior University. This software is
+ distributed under a BSD-style license. Please see the section LICENSE
+ below for more information.
+
+BLURB
+
+ lbcd is a daemon that runs on a UNIX system and answers UDP queries with
+ information about system load, number of logged-on users, uptime, and
+ free /tmp space. This information can be used to accumulate system
+ status across a cluster with light-weight queries or can be used as
+ input to a load-balancing system to choose the best system to which to
+ direct new incoming connections.
+
+DESCRIPTION
+
+ The lbcd daemon runs on a UNIX system and answers UDP queries (by
+ default on port 4330) with information about system load, number of
+ logged on users, uptime, and free /tmp space. This information can be
+ used to accumulate system status across a cluster with light-weight
+ queries or can be used as input to a load-balancing system to choose the
+ best system to which to direct new incoming connections. It was
+ designed for use with the lbnamed DNS load balancer, available at:
+
+ <http://www.stanford.edu/~riepel/lbnamed/>
+
+ It was originally written by Roland Schemers, was rewritten by Larry
+ Schwimmer to add protocol version 3 with some additional features and
+ service probing, and is currently maintained by Russ Allbery.
+
+ The information provided isn't particularly sophisticated, and a good
+ hardware load balancer will be able to consider such things as
+ connection latency and responsiveness to make better decisions, but lbcd
+ with lbnamed works quite well for smaller scale problems, scales well to
+ multiple load balance pools for different services, and is much simpler
+ and cheaper to understand and deploy.
+
+ Included in this package is a small client program, lbcdclient, which
+ can query an lbcd server and display a formatted version of the returned
+ information.
+
+REQUIREMENTS
+
+ lbcd is written in C, so you'll need a C compiler. It also uses kernel
+ calls to obtain load and uptime information, and at present has only
+ been ported to Linux, Solaris, AIX, various BSD systems, Mac OS X,
+ HP-UX, IRIX, and Tru64. It is currently primarily tested on Linux.
+ Platforms not listed may require some porting effort, as may old or
+ unusual platforms that aren't regularly tested.
+
+ The lbcdclient program requires Perl (any version should do).
+
+ To bootstrap from a Git checkout, or if you change the Automake files
+ and need to regenerate Makefile.in, you will need Automake 1.11 or
+ later. For bootstrap or if you change configure.ac or any of the m4
+ files it includes and need to regenerate configure or config.h.in, you
+ will need Autoconf 2.64 or later.
+
+INSTALLATION
+
+ You can build and install remctl with the standard commands:
+
+ ./configure
+ make
+ make install
+
+ Pass --enable-silent-rules to configure for a quieter build (similar to
+ the Linux kernel). Use make warnings instead of make to build with full
+ GCC compiler warnings (requires a relatively current version of GCC).
+
+ The last step will probably have to be done as root. By default, remctl
+ installs itself under /usr/local; you can change that path by passing
+ the --prefix=PATH argument to configure.
+
+ lbcd looks for $sysconfdir/nolbcd and returns the maximum load if that
+ file is present, allowing one to effectively drop a system out of a
+ load-balanced pool by touching that file. By default, the path is
+ /usr/local/etc/nolbcd, but you may want to pass --sysconfdir=/etc to
+ configure to use /etc/nolbcd.
+
+ lbcdclient is written in Perl, so you may have to edit the first line of
+ the script to point to the correct Perl location on your system. It
+ does not use any sophisticated Perl features or add-on modules.
+
+ You will generally want to start lbcd at system boot. All that is
+ needed is a simple init script to start lbcd with the appropriate
+ options or kill it again. It writes its PID into /var/run/lbcd.pid by
+ default (and this can be changed with the -P option). On many systems,
+ lbcd will need to run as root or as a member of particular groups to
+ obtain system load average and uptime information.
+
+TESTING
+
+ lbcd comes with a rudimentary test suite, which you can run after
+ building lbcd with:
+
+ make check
+
+ If a test case fails, please run the that individual test case with
+ verbose output using:
+
+ tests/runtests -o <name-of-test>
+
+ and send me the output when reporting the problem. Currently, the test
+ suite only checks the portability and utility libraries; tests for lbcd
+ and lbcdclient are coming in later releases.
+
+SUPPORT
+
+ The lbcd web page at:
+
+ http://www.eyrie.org/~eagle/software/lbcd/
+
+ will always have the current version of this package, the current
+ documentation, and pointers to any additional resources.
+
+ New lbcd releases are announced on the low-volume lbnamed-users mailing
+ list. To subscribe or see the list archives, go to:
+
+ https://mailman.stanford.edu/mailman/listinfo/lbnamed-users
+
+ I welcome bug reports and patches for this package at eagle@eyrie.org.
+ However, please be aware that I tend to be extremely busy and work
+ projects often take priority. I'll save your mail and get to it as soon
+ as I can, but it may take me a couple of months.
+
+SOURCE REPOSITORY
+
+ lbcd is maintained using Git. You can access the current source by
+ cloning the repository at:
+
+ git://git.eyrie.org/system/lbcd.git
+
+ or view the repository via the web at:
+
+ http://git.eyrie.org/?p=system/lbcd.git
+
+ When contributing modifications, patches (possibly generated by
+ git-format-patch) are preferred to Git pull requests.
+
+LICENSE
+
+ The lbcd package as a whole is covered by the following copyright
+ statement and license:
+
+ Copyright 1993-1994, 1996-1998, 2000, 2003-2009, 2012-2013
+ The Board of Trustees of the Leland Stanford Junior University
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ All individual files without an explicit exception below are released
+ under this license. Some files may have additional copyright holders as
+ noted in those files. There is detailed information about the licensing
+ of each file in the LICENSE file in this distribution.
+
+ Some files in this distribution are individually released under
+ different licenses, all of which are compatible with the above general
+ package license but which may require preservation of additional
+ notices. All required notices are preserved in the LICENSE file.
+
+ For any copyright range specified by files in this package as YYYY-ZZZZ,
+ the range specifies every single year in that closed interval.