1#!/usr/local/bin/perl
2
3use warnings;
4use strict;
5use Data::Dumper;
6use Getopt::Long;
7use File::Basename;
8use Sys::Hostname;
9
10$|=1;                                   # autoflush
11
12my %options = (
13               check => 0,
14               verbose => 0,
15               veryverbose => 0,
16               help => 0,
17               cfagent => "../cf-agent/cf-agent",
18               workdir => "/tmp",
19              );
20
21die ("Unknown options") unless GetOptions(\%options,
22           "help|h!",
23           "check|c!",
24           "cfagent=s",
25           "workdir=s",
26           "verbose|v!",
27           "veryverbose!",
28    );
29
30if ($options{help})
31{
32 print <<EOHIPPUS;
33Syntax: $0 [-c|--check] [-v|--verbose] [--cfagent=PATH] [--workdir=WORKDIR] FILE1.cf FILE2.cf ...
34
35Generate the output section of CFEngine code example.
36
37With -c or --check, the script reports if the output is different but does not
38write it.
39
40With -v or --verbose, the script shows the full output of each test.  Use
41--veryverbose if you REALLY want a lot of output.
42
43The --workdir path, defaulting to /tmp, is used for storing the example to be
44run.
45
46The --cfagent path, defaulting to ../cf-agent/cf-agent, is the cf-agent path.
47
48Each input .cf file is scanned for three markers:
49
501) required: a cfengine3 code block to be run
51
52#+begin_src cfengine3
53... CFEngine code to run here ...
54#+end_src
55
562) optionally, a prep block
57(each command will be run before the cfengine3 code block)
58
59#+begin_src prep
60#@ ```
61#@ touch -t '200102031234.56' /tmp/earlier
62#@ touch -t '200202031234.56' /tmp/later
63#@ ```
64#+end_src
65
663) required: an output code block
67
68#+begin_src example_output
69#@ ```
70#@ 2013-12-16T20:48:24+0200   notice: /default/example: R: The secret changes have been accessed after the reference time
71#@ ```
72#+end_src
73
74This block is rewritten if it's different, otherwise it's left alone.
75
76The "#@ " part is optional but needed if you want the file to be valid CFEngine
77policy. The "#@ ```" parts make the prep and output steps render as code in markdown.
78
79If the output is unpredictable due to, for example, random input or network
80dependencies, make sure the unpredictable line has the string RANDOM in capital
81letters somewhere. That will skip the output check for that line.
82
83EOHIPPUS
84
85  exit;
86}
87
88my $rc = 0;
89my @todo = @ARGV;
90
91foreach my $file (@todo)
92{
93    open my $fh, '<', $file or warn "Could not open $file: $!";
94    my $data = join '', <$fh>;
95    close $fh;
96    my $copy = $data;
97    if ($data =~ m/#\+begin_src cfengine3\n(.+?)\n#\+end_src/s)
98    {
99        my $example = $1;
100
101        my $prep;
102        if ($data =~ m/#\+begin_src prep\n(.+?)\n#\+end_src/s)
103        {
104            $prep = [split "\n", $1];
105        }
106
107        $data =~ s/(#\+begin_src example_output( no_check)?\n)(.*?)(#\+end_src)/$1 . rewrite_output($file, $prep, $example, $3) . $4/es;
108        if (!defined($2) && $data ne $copy)
109        {
110            print "$file: output differs from original...";
111            if ($options{check})
112            {
113                $rc = 1;
114                print "\n";
115                next;
116            }
117
118            open my $fh, '>', $file or warn "Could not open $file: $!";
119            print $fh $data;
120            close $fh;
121            print "new output written!\n";
122        }
123    }
124    else
125    {
126        warn "No example to run was found in $file, skipping";
127    }
128}
129
130exit $rc;
131
132sub rewrite_output
133{
134    my $file = shift @_;
135    my $prep = shift @_;
136    my $example = shift @_;
137    my $old_output = shift @_;
138    my $new_output = run_example($file, $prep, $example);
139
140    if (equal_outputs($old_output, $new_output, $file))
141    {
142        return $old_output;
143    }
144
145    if (defined $new_output && length $new_output > 0)
146    {
147        $new_output =~ s/^/#@ /mg;
148        $new_output = "#@ ```\n$new_output#@ ```\n";
149    }
150
151    return $new_output;
152}
153
154sub equal_outputs
155{
156    # strip out date, e.g. '2013-12-16T20:48:24+0200'
157    my $x = shift @_;
158    my $y = shift @_;
159    my $file = shift @_;
160
161    my ($tempfile, $base) = get_tempfile($file);
162
163    $x =~ s/^#@ ```\s+//mg;
164    $y =~ s/^#@ ```\s+//mg;
165
166    $x =~ s/^(#@ )//mg;
167    $x =~ s/^[-0-9T:+]+\s+//mg;
168    $y =~ s/^(#@ )//mg;
169    $y =~ s/^[-0-9T:+]+\s+//mg;
170
171    $x =~ s/.*RANDOM.*//mg;
172    $y =~ s/.*RANDOM.*//mg;
173
174    # strip leading blanks, for example from "  error:"
175    $x =~ s/^ *//mg;
176    $y =~ s/^ *//mg;
177
178    if ($x ne $y)
179    {
180        open my $fha, '>', "$tempfile.a" or die "Could not write to diff output $tempfile.a: $!";
181        print $fha $x;
182        close $fha;
183
184        open my $fhb, '>', "$tempfile.b" or die "Could not write to diff output $tempfile.b: $!";
185        print $fhb $y;
186        close $fhb;
187
188        system("diff -u $tempfile.a $tempfile.b") if $options{verbose};
189        return 0;
190    }
191
192    return 1;
193}
194
195sub get_tempfile
196{
197    my $file = shift @_;
198
199    my $base = basename($file);
200    my $tempfile = "$options{workdir}/$base";
201    mkdir $options{workdir} unless -e $options{workdir};
202
203    return ($tempfile, $base);
204}
205
206sub run_example
207{
208    my $file = shift @_;
209    my $prep = shift @_ || [];
210    my $example = shift @_;
211
212    my ($tempfile, $base) = get_tempfile($file);
213    open my $fh, '>', $tempfile or die "Could not write to $tempfile: $!";
214    print $fh $example;
215    close $fh;
216    chmod 0600, $tempfile;
217
218    foreach (@$prep)
219    {
220        s/^#@ //;
221        # skip Markdown markup like ```
222        next unless m/^\w/;
223        s/FILE/$tempfile/g;
224        s/\$HOSTNAME/hostname()/ge;
225        print "processing $file: Running prep '$_'"
226         if $options{verbose};
227        system($_);
228    }
229
230    $ENV{EXAMPLE} = $base;
231    $ENV{CFENGINE_COLOR} = 0;
232    my $cmd = "$options{cfagent} -D_cfe_output_testing -Kf $tempfile 2>&1";
233    open my $ofh, '-|', $cmd;
234    my $output = join '', <$ofh>;
235    close $ofh;
236
237    print "Test file: $file\nCommand: $cmd\n\nNEW OUTPUT: [[[$output]]]\n\n\n"
238     if $options{verbose};
239
240    return $output;
241}
242