Showing posts with label tools. Show all posts
Showing posts with label tools. Show all posts

Tuesday, November 01, 2011

Code coverage tool

Motivation for writing a coverage tool

In Factor, the old code-is-data adage is true and can be used to our advantage when writing runtime tools. Code is stored in what we call a quotation, which is just a sequence of functions (words) and data literals and can be called at runtime. A function (or word) in Factor is simply a named quotation, and typing that word name causes the quotation to run.

Since quotations in Factor do not terminate early unless an exception is thrown, we know that if each quotation gets run, then we have run all of the code. While this won't tell us if there are logic errors, and while we can still have code fail due to unexpected inputs, at least we can be sure that all of the code is getting exercise and there are no paths that never get run. We can use the results of this tool to write better unit tests.

Demo of the coverage tool

As a simple demo, there's a fun algorithm that produces a sequence of numbers from a start number, where the sequence always seems to end at one. For any integer greater than zero, the the Collatz conjecture states that this sequence will eventually reach the number one. The algorithm goes as follows: take any counting number (1, 2, ...) and either multiply by three and add 1, or divide by two, if the number is odd or even, respectively, and record the sequence of numbers until you reach the number one. This conjecture has been found to be true experimentally for every number up to 10^18, but no proof exists that it's true for all possible inputs.

For example, the Collatz sequence for 3 is: { 3 10 5 16 8 4 2 1 }

We can come up with a solution pretty easily (partially taken from the Project Euler #14, in extra/).

We do the command: "collatz" scaffold-work

Click on the link, edit the file ~/factor/extra/collatz/collatz.factor
USING: combinators.short-circuit kernel make math ;
IN: collatz

: next-collatz ( n -- n )
dup even? [ 2 / ] [ 3 * 1 + ] if ;

: collatz-unsafe ( n -- seq )
[ [ dup 1 > ] [ dup , next-collatz ] while , ] { } make ;

ERROR: invalid-collatz-input n ;

: collatz ( n -- seq )
dup { [ integer? ] [ 1 >= ] } 1&&
[ collatz-unsafe ]
[ invalid-collatz-input ] if ;
We're going to be extra careful here to demonstrate the code coverage tool, so I added error checking to make sure it's an integer greater than or equal to one.

Let's write some unit tests to make sure it works.

Run the command: "collatz" scaffold-tests
Now click on the link to edit the file it created, ~/factor/extra/collatz/collatz-tests.factor
USING: tools.test collatz ;
IN: collatz.tests

[ { 1 } ] [ 1 collatz ] unit-test
[ { 2 1 } ] [ 2 collatz ] unit-test
[ { 3 10 5 16 8 4 2 1 } ] [ 3 collatz ] unit-test
If we run "collatz" test, we see that all tests pass.

Now, let's see how well we did with code coverage.

We run the command:
IN: scratchpad USE: tools.coverage "collatz" test-coverage .
Loading resource:work/collatz/collatz-tests.factor
Unit Test: { [ { 1 } ] [ 1 collatz ] }
Unit Test: { [ { 2 1 } ] [ 2 collatz ] }
Unit Test: { [ { 3 10 5 16 8 4 2 1 } ] [ 3 collatz ] }
{
{ next-collatz { } }
{ collatz { [ invalid-collatz-input ] } }
{
invalid-collatz-input
{ [ \ invalid-collatz-input boa throw ] }
}
{ collatz-unsafe { } }
}
What this tells us is we had quotations in the collatz word and the invalid-collatz-input words that did not get called. Of course -- we never passed it anything other than valid inputs. How about passing it a string or a negative integer?

Now the result looks better:
IN: scratchpad "collatz" test-coverage .
Loading resource:work/collatz/collatz-tests.factor
Unit Test: { [ { 1 } ] [ 1 collatz ] }
Unit Test: { [ { 2 1 } ] [ 2 collatz ] }
Unit Test: { [ { 3 10 5 16 8 4 2 1 } ] [ 3 collatz ] }
Must Fail With: { [ "hello world" collatz ] [ invalid-collatz-input? ] }
Must Fail With: { [ -50 collatz ] [ invalid-collatz-input? ] }
{
{ next-collatz { } }
{ collatz { } }
{ invalid-collatz-input { } }
{ collatz-unsafe { } }
}
We can even get a number for how well tests cover a vocabulary:
"collatz" %coverage .
1.0


The implementation of the coverage tool

Every word in Factor stores its definition in the `def' slot. If we examine this slot, we see that it's a quotation that may contain other quotations. Using the annotation vocabulary, we can add code that executes before and after the code in the quotation. What the coverage tool does is adds a container that stores a boolean flag at the beginning of each quotation, and when the quotation gets run, the flag is set to true. This tool can be turned on and off, independent of the containers being in place, with the coverage-on and coverage-off words.

Here's a word that turns a Roman numeral into an integer:
\ roman> def>> .
[ >lower [ roman>= ] monotonic-split [ (roman>) ] map-sum ]
After annotating it, the new definition looks like:
\ roman> add-coverage \ roman> def>> .
[
T{ coverage } flag-covered >lower
[ T{ coverage } flag-covered roman>= ] monotonic-split
[ T{ coverage } flag-covered (roman>) ] map-sum
]
The flags are all set to false right now. After turning on the flag to let enable the coverage code and running the word, we see a change:
coverage-on "iii" roman> drop \ roman> def>> .[
T{ coverage { executed? t } } flag-covered >lower
[ T{ coverage { executed? t } } flag-covered roman>= ]
monotonic-split
[ T{ coverage { executed? t } } flag-covered (roman>) ]
map-sum
]
Notice that all of the coverage containers have been executed. To generate the report, we simply iterate over each word and collect all of the quotations where this flag has not been set -- these quotations never ran.

In writing this article, I realized that each quotation should have a flag on exit as well, in case an exception gets thrown in the middle of executing this quotation and control never reaches the end. Partially-executed quotations will soon be reported by the tool, after I make this fix.

I hope you can use this tool to improve your Factor code. Have fun!

Tuesday, January 13, 2009

Files and file-systems in Factor, part 2

In my previous post I wrote about the file-info and file-system-info words. Using those words, it's possible to write a portable program for directory listing (dir or ls) and one for file-systems listing (df). Uses for directory listing include file-system utility programs and FTP servers.

File listing tool

The file-listing tool is in the Factor git repository as basis/tools/files/files.factor. Which slots you see depend on how it is configured and on which platform it's running. Directory listings can be sorted by slot and the default sort is by name.

On Unix platforms, it's configured like this:
    <listing-tool>
{ permissions nlinks user group file-size file-date file-name } >>specs
{ { directory-entry>> name>> <=> } } >>sort
Listing a directory on MacOSX:
-rw-r--r-- 1  erg staff 27185 Nov 28  2008 #factor.el#
drwxr-xr-x 6 erg staff 204 Nov 17 2008 Factor.tmbundle
-rw-r--r-- 1 erg staff 30282 Nov 30 2008 factor.el
-rw-r--r-- 1 erg staff 18037 Nov 17 2008 factor.vim
-rw-r--r-- 1 erg staff 12496 Nov 17 2008 factor.vim.fgen
drwxr-xr-x 26 erg staff 884 Jan 13 11:17 fuel
drwxr-xr-x 8 erg staff 272 Nov 17 2008 icons
Windows platforms take the look of the ``dir'' command by default:
2009-01-14 00:00:53 <DIR>                Factor.tmbundle
2009-01-14 00:00:53 30282 factor.el
2009-01-14 00:00:53 18037 factor.vim
2009-01-14 00:00:53 12496 factor.vim.fgen
2009-01-14 00:00:53 <DIR> fuel
2009-01-14 00:00:53 <DIR> icons

File-systems tool

A tool for disk usage and mounted devices was easy to write once file-system tuples worked everywhere. Once again, it's configurable for whatever you want to see. The default file-system word is:
: file-systems. ( -- )
{
device-name available-space free-space used-space
total-space percent-used mount-point
} print-file-systems ;
File-systems on a Mac:
device-name   available-space free-space   used-space   total-space  percent-used mount-point
/dev/disk0s2 118599725056 118861869056 200867090432 319728959488 62 /
fdesc 0 0 1024 1024 100 /dev
fdesc 0 0 1024 1024 100 /dev
map -hosts 0 0 0 0 0 /net
map auto_home 0 0 0 0 0 /home

On Windows:
device-name      available-space free-space used-space total-space percent-used mount-point
3814506496 3814506496 6911225856 10725732352 64 C:\
VBOXADDITIONS_2. 0 0 27983872 27983872 100 D:\
0 0 0 0 0 A:\

Wednesday, September 03, 2008

Vocabulary and documentation tool: tools.scaffold

The Factor project is on a documentation binge until all of the core and basis vocabularies are documented. I noticed, as anyone tasked to write a bunch of documenation would, that quite a bit of it could be automated. To begin writing docs for a vocabulary that already exists, you can just run the tool in it and edit the generated help file.

I actually ended up writing two tools -- one for the docs, and another to create new vocabularies with a simple command.

Scaffold tool to create new vocabularies


First, an aside. Factor's module system is based on a directory layout, with several vocabulary roots which are stored in the vocab-roots symbol.
( scratchpad ) USE: vocabs.loader vocab-roots get .
V{ "resource:core" "resource:basis" "resource:extra" "resource:work" }
Knowing this, you can now create a new vocabulary and boilerplate empty Factor files from inside of Factor and click the links to edit them in your favorite editor.
( scratchpad ) "extra" "alchemy" scaffold-vocab
Creating scaffolding for P" resource:extra/alchemy/alchemy.factor"
Creating scaffolding for P" resource:extra/alchemy/alchemy-tests.factor"
Creating scaffolding for P" resource:extra/alchemy/authors.txt"

( scratchpad ) "extra" "alchemy" scaffold-help
Creating scaffolding for P" resource:extra/alchemy/alchemy-docs.factor"
The scaffold tool is programmed not to overwrite files if they already exist.

Scaffold tool for documenation



Say I have a Factor word that turns lead into gold:
: lead>gold ( lead -- gold ) ... ;

The implementation of this word is left as an exercise to the reader. We don't have to understand how it works to document what it does.

Without an automation tool, you would begin by creating a new file (extra/alchemy/alchemy-docs.factor) in the same directory as your code file (extra/alchemy/alchemy.factor). What follows is the standard bolierplate for documentation. Notice that documenation goes in the same vocabulary as your actual code, but comes from different files on disk.

! Copyright (C) 2008 Doug Coleman.
! See http://factorcode.org/license.txt for BSD license.
USING: help.markup help.syntax ;
IN: alchemy
Every doc file has such a header based on the date, who you are, and the vocabulary name. With those three pieces of information, the above can be auto-generated. I added a new symbol developer-name in tools.scaffold that should be set to your name when writing docs.

Now let's look at the documenation for lead>gold:
HELP: lead>gold
{ $values { "lead" "boring lead" } { "gold" "shiny gold" } }
{ $description "Turns lead into gold." } ;


Notice that we know the word name, the first strings in each stack effect in the $values array, and the markup structure.

Here is the generated scafford:
HELP: lead>gold
{ $values { "lead" null } { "gold" null } }
{ $description } ;
This is much less wrist punishment than typing manually. The scaffold tool can even generate the documentation with their Factor types given correct code and standard type names like "quot" and "hashtable". Types that are not known are given the null class. Documenation correctness tools can later search for null objects and report them as errors, since no words should operate on this type.

I'll finish by including a snippet of the output of running the scaffold help generator on itself.
! Copyright (C) 2008 Doug Coleman.
! See http://factorcode.org/license.txt for BSD license.
USING: arrays help.markup help.syntax io.streams.string kernel strings words ;
IN: tools.scaffold

HELP: check-root
{ $values
{ "string" string }
{ "string" string } }
{ $description } ;

HELP: check-scaffold
{ $values
{ "vocab-root" "a vocabulary root string" } { "string" string }
{ "vocab-root" "a vocabulary root string" } { "string" string } }
{ $description } ;

HELP: check-vocab-name
{ $values
{ "string" string }
{ "string" string } }
{ $description } ;

HELP: developer-name
{ $description } ;

HELP: help.
{ $values
{ "word" word } }
{ $description } ;

HELP: lookup-type
{ $values
{ "string" string }
{ "object/string" null } { "?" "a boolean" } }
{ $description } ;

HELP: main-file-string
{ $values
{ "vocab" "a vocabulary specifier" }
{ "string" string } }
{ $description } ;

HELP: not-a-vocab-root
{ $values
{ "string" string } }
{ $description } ;

HELP: not-a-vocab-root?
{ $values
{ "object" object }
{ "?" "a boolean" } }
{ $description } ;

HELP: root?
{ $values
{ "string" string }
{ "?" "a boolean" } }
{ $description } ;

HELP: scaffold-authors
{ $values
{ "path" "a pathname string" } }
{ $description } ;

HELP: scaffold-copyright
{ $description } ;

HELP: tests-file-string
{ $values
{ "vocab" "a vocabulary specifier" }
{ "string" string } }
{ $description } ;

HELP: using
{ $description } ;

HELP: vocab>scaffold-path
{ $values
{ "vocab-root" "a vocabulary root string" } { "string" string }
{ "path" "a pathname string" } }
{ $description } ;

ARTICLE: "tools.scaffold" "tools.scaffold"
;

ABOUT: "tools.scaffold"
I hope this new vocabulary makes documenation a lot less tedious and error-prone. Now to go write some docs...