Compare commits

...
Author SHA1 Message Date
swaits 1e54b04490 flow: Closed <feature> 'improve_test_coverage'. 2015-02-15 16:04:09 -08:00
swaits ccc65f9715 coverage improved from 85.2% to 91.1% 2015-02-15 16:03:10 -08:00
swaits e4ce693396 flow: Created branch 'feature/improve_test_coverage'. 2015-02-15 15:38:01 -08:00
swaits cca2bcaf58 flow: Merged <feature> 'create_simple_sitemap' to <develop> ('develop'). 2015-02-15 15:31:27 -08:00
swaits f058de8e6f flow: Closed <feature> 'create_simple_sitemap'. 2015-02-15 15:31:27 -08:00
swaits 8a71e4cb5c missed a go fmt 2015-02-15 15:30:13 -08:00
swaits c95ad7ab95 now crawling multiple pages!
* moved Page{} and NewPage() into page.go
* queued and crawled are both maps now. used to track which pages we already know about
* tests updated to match the multipage crawling
* added another file to the testsite (scripts/blah.js)
2015-02-15 15:26:21 -08:00
swaits 0c48b94ece refactoring and cleanup. needed before proceeding.
* fetch() renamed to fetchPage. now takes *Page instead of url string
* fetch_filetype() renamed to fetchFiletype. now takes *Page instead of url string
* parse() renamed to parseLinks()
* tests updated appropriately
* minor cleanup (lint)
* NewPage added for conveniently creating new *Page structs from url strings
2015-02-15 14:54:52 -08:00
swaits 614f05f1d7 minor Makefile cleanup 2015-02-15 14:02:08 -08:00
swaits 9fb19b0067 Merge from develop. Plus hack fix to make golint work again. 2015-02-15 12:59:52 -08:00
swaits d8b47abf6e flow: Merged <feature> 'add_test_coverage' to <develop> ('develop'). 2015-02-15 12:41:02 -08:00
swaits 67817048f9 flow: Closed <feature> 'add_test_coverage'. 2015-02-15 12:41:02 -08:00
swaits 12cd121271 add coverage to Makefile (make cover)
* also tweaked GOPATH as needed
* re-vendored with cover tool included
* ignore $/cover.out
2015-02-15 12:40:56 -08:00
swaits dbf49d259b flow: Created branch 'feature/add_test_coverage'. 2015-02-15 12:18:00 -08:00
swaits 0456723f0b minimally functional crawler - crawls single page, finds links, stores in data structure
* refined Page struct. now need to refactor everything else to use this
* wrote docrawl() function. NOT intended to be anything like the final design. just sketching it out to get a better idea of needs, design, etc.
* replaced fmt.Print* with t.Log* in tests (oops!)
* added a header fetcher function to get the file type (since we only want to parse html (with tests)
* added small test for the URL resolver where the base URL is empty
* added placeholder files in testsite/ for about.html and image.png
2015-02-15 12:12:30 -08:00
swaits 7f6978ad27 Merge from develop 2015-02-15 10:03:36 -08:00
swaits d60700bf1a flow: Merged <feature> 'url_parse_and_clean' to <develop> ('develop'). 2015-02-15 10:02:25 -08:00
swaits 2b58869a04 Merge from develop 2015-02-15 08:08:23 -08:00
swaits d1f14120df Merge from develop 2015-02-15 07:48:10 -08:00
swaits 5ef46be8c4 add (presently failing) tests for discovering a simple site map 2015-02-15 00:58:01 -08:00
swaits 249aca229b flow: Created branch 'feature/create_simple_sitemap'. 2015-02-15 00:43:50 -08:00
17 changed files with 308 additions and 35 deletions

No files matched your search

+2 -1
View File
@@ -1,2 +1,3 @@
syntax: glob
bin
bin
cover.out
Binary file not shown.
+18 -9
View File
@@ -3,18 +3,20 @@
# derived from: http://zduck.com/2014/go-project-structure-and-dependencies/
# build targets (which aren't files)
.PHONY: build fmt lint vet test autotest run clean env vendor
.PHONY: build fmt lint vet test autotest cover doc run clean env vendor
# configuration
APPNAME := docrawler
VENDOR := .vendor
PACKAGES := \
golang.org/x/tools/cmd/cover \
golang.org/x/tools/cmd/godoc \
golang.org/x/tools/cmd/vet \
github.com/golang/lint/golint
# override GOPATH to be our vendor directory
export GOPATH := ${PWD}/${VENDOR}
export PATH := ${PWD}/${VENDOR}/bin:${PATH}
export GOPATH := ${PWD}/${VENDOR}:${PWD}
export PATH := ${PWD}/${VENDOR}/bin:${PWD}/bin:${PATH}
default: build
@@ -25,7 +27,8 @@ fmt:
go fmt ./src/...
lint:
golint ./src
# TODO golint is not accepting the "./src/..." for some reason (from within make)
golint ./src/${APPNAME}/*.go
vet:
go vet ./src/...
@@ -35,24 +38,30 @@ test:
autotest:
fswatch -or ./src | \
xargs -n1 -I{} ${SHELL} -c 'echo ; date ; echo ----------------------------; go test ./src/... | ./util/testfilter.sh'
xargs -n1 -I{} ${SHELL} -c 'echo ; date ; echo ----------------------------; go test ./src/... | ./util/testfilter.sh'
cover:
go test -coverprofile cover.out ./src/... && go tool cover -html cover.out
doc:
godoc -http=:6060 -index
run: build
./bin/${APPNAME}
clean:
rm -f ./bin/${APPNAME}
rm -f ./bin/${APPNAME} ./cover.out
env:
go env
# vendor
# * wipes our vendor directory completely
# * fetches all packages in ${PACKAGES}
# * fetches all packages in ${PACKAGES} (GOPATH hardcoded to vendor directory only)
# * removes source control dirs so packages are vendorable (commitable)
vendor:
rm -dRf ./${VENDOR}/src && \
go get -u ${PACKAGES} && \
rm -dRf ./${VENDOR} && \
GOPATH=${PWD}/${VENDOR} go get -u ${PACKAGES} && \
rm -rf `find ./${VENDOR}/src -type d -name .git` && \
rm -rf `find ./${VENDOR}/src -type d -name .hg` && \
rm -rf `find ./${VENDOR}/src -type d -name .bzr` && \
+56
View File
@@ -2,5 +2,61 @@ package main
import ()
// docrawl begins crawling the site at "url"
func docrawl(url string) []*Page {
// our result structure
var sitemap []*Page
// what we've already parsed, what we still need to parse
crawled := make(map[string]*Page)
queued := make(map[string]*Page)
// create first page and add to the queue
homepage, _ := NewPage(nil, url) // TODO check error
queued[homepage.URL.String()] = homepage
// main loop
for len(queued) > 0 {
// pop a page off of queued
var dest *Page
var destURL string
for destURL, dest = range queued {
break
}
delete(queued, destURL)
// mark this page as complete
crawled[dest.URL.String()] = dest
// fetch page
text, err := fetchPage(dest)
if err != nil {
continue // TODO handle
}
// parse links
_, links, err := parseLinks(text)
if err != nil {
continue // TODO handle
}
for _, l := range links {
p, _ := NewPage(dest, l) // TODO check error
dest.Children = append(dest.Children, p)
// see if we should queue this new page for crawling
_, haveCrawled := crawled[p.URL.String()]
_, haveQueued := queued[p.URL.String()]
if !haveCrawled && !haveQueued {
queued[p.URL.String()] = p
}
}
// add page to list
sitemap = append(sitemap, dest)
}
return sitemap
}
func main() {
}
+72 -9
View File
@@ -1,7 +1,6 @@
package main
import (
"fmt"
"net"
"net/http"
"os"
@@ -43,8 +42,8 @@ func TestMain(m *testing.M) {
func TestServerRunning(t *testing.T) {
resp, err := http.Get(baseURL)
if resp.StatusCode != 200 || err != nil {
fmt.Printf("got %v, err = %v\n", resp.StatusCode, err)
t.Error(err)
t.Logf("got %v, err = %v\n", resp.StatusCode, err)
t.Fatal(err)
}
}
@@ -52,21 +51,85 @@ func TestServerRunning(t *testing.T) {
func TestBadRequest(t *testing.T) {
resp, err := http.Get(baseURL + "doesnotexist.html")
if resp.StatusCode != 404 || err != nil {
fmt.Printf("got %v, err = %v\n", resp.StatusCode, err)
t.Error("request for non existent file didn't fail properly")
t.Logf("got %v, err = %v\n", resp.StatusCode, err)
t.Fatal("request for non existent file didn't fail properly")
}
}
// TestSimpleFetch makes sure we can fetch a file and we get exactly what we expect
func TestSimpleFetch(t *testing.T) {
body, err := fetch(baseURL + "fetch_test.html")
page, err := NewPage(nil, baseURL+"fetch_test.html")
if err != nil {
t.Error(err)
t.Error("problem creating New Page struct")
}
body, err := fetchPage(page)
if err != nil {
t.Fatal(err)
}
desired := "<html><head></head><body></body></html>\n"
if body != desired {
fmt.Printf(" Got: '%v'\n", body)
fmt.Printf("Wanted: '%v'\n", desired)
t.Logf(" Got: '%v'\n", body)
t.Logf("Wanted: '%v'\n", desired)
t.Error("fetch succeeded, but content mismatched")
}
}
// TestSimpleMap figures out the site map for the site in baseURL
func TestSimpleMap(t *testing.T) {
pages := docrawl(baseURL)
t.Log("pages = ", pages)
if len(pages) != 4 {
t.Logf("got %v, wanted %v\n", len(pages), 1)
t.Fatal("got wrong number of pages")
}
if pages[0].URL.String() != baseURL {
t.Error("page URL is invalid")
}
if len(pages[0].Children) != 3 {
t.Logf("got %v, wanted %v\n", len(pages[0].Children), 1)
t.Fatal("got wrong number of links")
}
if pages[0].Children[0].URL.String() != baseURL+"about.html" {
t.Logf(" Got: %q\n", pages[0].Children[0].URL.String())
t.Logf("Wanted: %q\n", baseURL+"about.html")
t.Error("link name is incorrect")
}
if pages[0].Children[1].URL.String() != baseURL+"assets/image.png" {
t.Logf(" Got: %q\n", pages[0].Children[1].URL.String())
t.Logf("Wanted: %q\n", baseURL+"assets/image.png")
t.Error("link name is incorrect")
}
if pages[0].Children[2].URL.String() != baseURL+"scripts/blah.js" {
t.Logf(" Got: %q\n", pages[0].Children[2].URL.String())
t.Logf("Wanted: %q\n", baseURL+"scripts/blah.js")
t.Error("link name is incorrect")
}
}
// TestHeaderFetching tests fetchFiletype() to see if we are getting expected results
func TestHeaderFetching(t *testing.T) {
basepage, err := NewPage(nil, baseURL)
if err != nil {
t.Error("problem creating New Page struct")
}
if err := fetchFiletype(basepage); err != nil || basepage.MediaType != "text/html" {
t.Error("problem fetching filetype")
}
page, err := NewPage(basepage, "about.html")
if err != nil {
t.Error("problem creating New Page struct")
}
if err := fetchFiletype(page); err != nil || page.MediaType != "text/html" {
t.Error("problem fetching filetype")
}
page, err = NewPage(basepage, "assets/image.png")
if err != nil {
t.Error("problem creating New Page struct")
}
if err := fetchFiletype(page); err != nil || page.MediaType != "text/plain" {
t.Logf("got %q, wanted %q", page.MediaType, "text/plain")
t.Error("problem fetching filetype")
}
}
+41 -3
View File
@@ -1,14 +1,52 @@
package main
import (
"errors"
"io/ioutil"
"mime"
"net/http"
)
// fetch takes a URL, GETs it, and returns the body as a string
func fetch(url string) (string, error) {
var (
errContentTypeNotFound = errors.New("no Content-Type header found")
)
// fetchFiletype performs an http HEAD to get the media type, and sets it
// directly in Page.MediaType
func fetchFiletype(page *Page) error {
resp, err := http.Head(page.URL.String())
if err != nil {
return err
}
// pull the content type out of the http header
contentType, ok := resp.Header["Content-Type"]
if !ok {
// no Content-Type header found
return errContentTypeNotFound
}
// parse mime type
mediatype, _, err := mime.ParseMediaType(contentType[0])
if err != nil {
return err
}
// set and return
page.MediaType = mediatype
return nil
}
// fetchPage takes a Page, GETs it, and returns the body as a string
func fetchPage(page *Page) (string, error) {
// figure out the file type
err := fetchFiletype(page)
if err != nil {
return "", err
}
// GET the url
resp, err := http.Get(url)
resp, err := http.Get(page.URL.String())
if err != nil {
return "", err
}
+27
View File
@@ -0,0 +1,27 @@
package main
import (
"testing"
)
// TestFailedHeaderFetching tests fetchFiletype() to see if we are getting expected failures
func TestFailedHeaderFetching(t *testing.T) {
page, err := NewPage(nil, "http://doesntexist23492387492837492374982734.com/")
if err != nil {
t.Error("problem creating New Page struct")
}
if err := fetchFiletype(page); err == nil {
t.Error("tired fetching bogus page but didn't get nil back from fetchFiletype")
}
}
// TestFailedFetching tests fetchFiletype() to see if we are getting expected failures
func TestFailedFetching(t *testing.T) {
page, err := NewPage(nil, "http://doesntexist23492387492837492374982734.com/")
if err != nil {
t.Error("problem creating New Page struct")
}
if _, err := fetchPage(page); err == nil {
t.Error("tired fetching bogus page but didn't get nil back from fetchPage")
}
}
+32
View File
@@ -0,0 +1,32 @@
package main
import (
"net/url"
)
// Page is a struct which defines a single page, which URLs (links and assets) it contains, etc.
type Page struct {
URL *url.URL
Base *Page
Title string
MediaType string
Children []*Page
}
// NewPage takes a referring Page + a URL and returns a new &Page{}
func NewPage(referrer *Page, rawurl string) (*Page, error) {
// determine the base URL so we can resulve this into a full URL
baseURL := ""
if referrer != nil {
baseURL = referrer.URL.String()
}
// resolve URL into a full absolute URL (non-relative)
u, err := resolveURL(baseURL, rawurl)
if err != nil {
return nil, err
}
// create struct and return
return &Page{URL: u}, nil
}
+13
View File
@@ -0,0 +1,13 @@
package main
import (
"testing"
)
// TestBadNewPage tests NewPage to see if we are getting expected failures
func TestBadNewPage(t *testing.T) {
_, err := NewPage(nil, "h%20t\tp://doesntexist23492387492837492374982734.com/")
if err == nil {
t.Error("tried creating a NewPage with an invalid URL but didn't get an error")
}
}
+2 -2
View File
@@ -10,9 +10,9 @@ import (
var reURL = regexp.MustCompile(`(?i)(src|href|xhref)\s*=\s*"([^"]+)"`) // TODO make RFC compliant
var reTitle = regexp.MustCompile(`(?i)<\s*title\s*>([^<]*)<\s*\/\s*title`)
// parse takes a string and attempts to parse any html title and all links out of it,
// parseLinks takes a string and attempts to parse any html title and all links out of it,
// and returns a sorted slice of the captures found
func parse(s string) (string, []string, error) {
func parseLinks(s string) (string, []string, error) {
// find the title, or default to empty
title := ""
titleMatches := reTitle.FindStringSubmatch(s)
+7 -3
View File
@@ -12,17 +12,18 @@ func TestSimpleParse(t *testing.T) {
</head>
<body>
<img src="/assets/image.png"/>
<a href="/about.html">
<a href="/about.html">fooey!</a>
<script href="scripts/blah.js"/>
</body>
</html>`
title, matches, err := parse(doc)
title, matches, err := parseLinks(doc)
if err != nil {
t.Error(err)
}
if title != "Test Page" {
t.Error("got wrong title")
}
if len(matches) != 2 {
if len(matches) != 3 {
t.Error("invalid number of matches in parse")
}
if matches[0] != "/about.html" {
@@ -31,4 +32,7 @@ func TestSimpleParse(t *testing.T) {
if matches[1] != "/assets/image.png" {
t.Error("match text is invalid")
}
if matches[2] != "scripts/blah.js" {
t.Error("match text is invalid")
}
}
+11
View File
@@ -0,0 +1,11 @@
<html>
<head>
<title>About Test</title>
<script src="scripts/blah.js"/>
</head>
<body>
<img src="/assets/image.png"/>
<a href="/">fooey!</a>
<a href="http://doesntexist23492387492837492374982734.com/">bad one</a>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

+3 -1
View File
@@ -1,8 +1,10 @@
<html>
<head>
<title>Home</title>
</head>
<body>
<img src="/assets/image.png"/>
<a href="/about.html">
<a href="/about.html">fooey!</a>
<script src="scripts/blah.js"/>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
+2 -2
View File
@@ -7,7 +7,7 @@ import (
)
var (
ErrInvalidURL = errors.New("this URL can't be parsed successfully")
errInvalidURL = errors.New("this URL can't be parsed successfully")
)
// cleanURL takes a URL and normalizes it by downcasing the host part
@@ -21,7 +21,7 @@ func checkURL(u *url.URL) error {
// check for non-empty Host and Scheme
if len(u.Host) == 0 || len(u.Scheme) == 0 {
// something about this URL doesn't meet our spec
return ErrInvalidURL
return errInvalidURL
}
return nil // success!
}
+21 -5
View File
@@ -1,7 +1,6 @@
package main
import (
"fmt"
"net/url"
"testing"
)
@@ -14,8 +13,8 @@ func TestURLCleaner(t *testing.T) {
}
cleanURL(u)
if u.Host != "somehost.com:8765" {
fmt.Printf(" Got: %q\n", u.Host)
fmt.Printf("Wanted: %q\n", "somehost.com:8765")
t.Logf(" Got: %q\n", u.Host)
t.Logf("Wanted: %q\n", "somehost.com:8765")
t.Error("host extraction failed")
}
if u.String() != "http://swaits:pass@somehost.com:8765/blah/blah.html?x=y#foo" {
@@ -41,8 +40,8 @@ func doResolveTest(t *testing.T, wanted, referrer, current string) {
t.Error("resolveURL failed (probably parsing)")
}
if u.String() != wanted {
fmt.Printf(" Got: %q\n", u)
fmt.Printf("Wanted: %q\n", wanted)
t.Logf(" Got: %q\n", u)
t.Logf("Wanted: %q\n", wanted)
t.Error("url resolution with base URL failed")
}
}
@@ -51,4 +50,21 @@ func TestURLResolution(t *testing.T) {
doResolveTest(t, "http://base.com/some/crazy/path/test.png", "http://BaSe.CoM/some/crazy/path/index.html?parm=x&blah=foo", "test.png")
doResolveTest(t, "http://S:D@base.com:1/test.png", "http://S:D@BaSe.CoM:1/some/crazy/path/index.html?parm=x&blah=foo", "/test.png")
doResolveTest(t, "http://another.com/blah.html", "http://S:D@BaSe.CoM:1/some/crazy/path/index.html?parm=x&blah=foo", "http://aNOThER.CoM/blah.html")
doResolveTest(t, "http://empty.com/blah.html", "", "http://empty.com/blah.html")
}
// TestBadResolve tries to resolve bogus URLs to be sure we're getting errors
/* TODO not sure how to make url.Parse fail in these cases!!!
func TestBadResolve(t* testing.T) {
u, err := resolveURL("http://a.com/", "h%20t\tp://doesnte%20xist23492387492837492374982734.com/")
if err == nil {
t.Logf("got %q", u.String())
t.Error("tried resolveURL with an invalid URL but didn't get an error")
}
u, err = resolveURL("h%20t\tp://doesnte%20xist23492387492837492374982734.com/", "http://a.com/")
if err == nil {
t.Logf("got %q", u.String())
t.Error("tried resolveURL with an invalid URL but didn't get an error")
}
}
*/