Fix up several tests for Travis compatibility and improved debugging

This includes manually setting test file permissions to be consistent
with what we expect and renaming test directories to more test-specific
names.

Add several new tests for manifest creation

Fix several issues identified by tests

* missing debuginfo checks
* track manifest file count
* perform format bump checks
* track and deprecate ghosted files

Signed-off-by: Matthew Johnson <matthew.johnson@intel.com>
This commit is contained in:
Matthew Johnson
2018-01-05 16:16:56 -08:00
committed by tmarcu
parent 774c589dff
commit 296a03aa22
63 changed files with 504 additions and 46 deletions
+2 -1
View File
@@ -19,4 +19,5 @@ missing
mixer-tools-*.tar.xz
swupd-server.log.*
coverage.out
test-web-dir/
testdata-*/
!testdata-*.bak/
+17 -7
View File
@@ -102,6 +102,10 @@ func processBundles(ui UpdateInfo, c config) ([]*Manifest, error) {
continue
}
if c.debuginfo.banned {
newM.removeDebuginfo(c.debuginfo)
}
// detect type changes
// fail out here if a type change is detected since this is not yet supported in client
if newM.hasTypeChanges() {
@@ -132,6 +136,7 @@ func processBundles(ui UpdateInfo, c config) ([]*Manifest, error) {
// this must be done after subtractManifests has been done for all manifests
// because subtractManifests sorts the file lists by filename alone
bundle.sortFilesVersionName()
bundle.Header.FileCount = uint32(len(bundle.Files))
}
return newManifests, nil
@@ -143,6 +148,8 @@ func CreateManifests(version uint32, minVersion bool, format uint, statedir stri
StateDir = statedir
}
c := getConfig()
if minVersion {
MinVersion = true
}
@@ -152,8 +159,6 @@ func CreateManifests(version uint32, minVersion bool, format uint, statedir stri
return err
}
c := getConfig()
var groups []string
if groups, err = readGroupsINI(filepath.Join(StateDir, "groups.ini")); err != nil {
return err
@@ -167,16 +172,20 @@ func CreateManifests(version uint32, minVersion bool, format uint, statedir stri
return err
}
if err = initBuildDirs(version, groups, c.imageBase); err != nil {
return err
}
oldFullManifest := Manifest{}
oldFullManifestPath := filepath.Join(StateDir, fmt.Sprint(lastVersion), "Manifest.full")
oldFullManifestPath := filepath.Join(c.outputDir, fmt.Sprint(lastVersion), "Manifest.full")
if err = oldFullManifest.ReadManifestFromFile(oldFullManifestPath); err != nil {
// might not exist, so the empty manifest is fine
}
if oldFullManifest.Header.Format > format {
return fmt.Errorf("new format %v is lower than old format %v", format, oldFullManifest.Header.Format)
}
if err = initBuildDirs(version, groups, c.imageBase); err != nil {
return err
}
// create new chroot from all bundle chroots
// TODO: this should be its own thing that an earlier step in mixer does
if err = createNewFullChroot(version, groups, c.imageBase); err != nil {
@@ -243,6 +252,7 @@ func CreateManifests(version uint32, minVersion bool, format uint, statedir stri
}
}
newMoM.Header.FileCount = uint32(len(newMoM.Files))
newMoM.sortFilesVersionName()
// write MoM
+289 -7
View File
@@ -5,6 +5,8 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
)
@@ -71,28 +73,308 @@ func TestInitBuildDirs(t *testing.T) {
}
}
func fileContains(path string, sub string) bool {
b, err := ioutil.ReadFile(path)
if err != nil {
return false
}
return strings.Contains(string(b), sub)
}
func fileContainsRe(path string, re *regexp.Regexp) string {
b, err := ioutil.ReadFile(path)
if err != nil {
return ""
}
return re.FindString(string(b))
}
func TestCreateManifests(t *testing.T) {
testDir := "./testdata/testdata-basic"
// init test dir
if err := os.RemoveAll("./testdata/test-web-dir"); err != nil {
t.Fatal("Unable to remove testdata/test-web-dir")
if err := os.RemoveAll(testDir); err != nil {
t.Fatalf("Unable to remove %s", testDir)
}
cmd := exec.Command("cp", "-a", "./testdata/test-web-dir.bak", "./testdata/test-web-dir")
cmd := exec.Command("cp", "-a", testDir+".bak", testDir)
if err := cmd.Run(); err != nil {
t.Fatal("Unable to copy testdata/test-web-dir.bak to testdata/test-web-dir")
t.Fatalf("Unable to copy %s.bak to %s", testDir, testDir)
}
if err := CreateManifests(10, false, 1, "./testdata/test-web-dir"); err != nil {
if err := CreateManifests(10, false, 1, testDir); err != nil {
t.Error(err)
}
// set last version to 10
ver := []byte("10\n")
if err := ioutil.WriteFile("./testdata/test-web-dir/image/LAST_VER", ver, 0755); err != nil {
if err := ioutil.WriteFile(filepath.Join(testDir, "image/LAST_VER"), ver, 0755); err != nil {
t.Fatal(err)
}
if err := CreateManifests(20, false, 1, "./testdata/test-web-dir"); err != nil {
if err := CreateManifests(20, false, 1, testDir); err != nil {
t.Error(err)
}
lines := []struct {
sub string
expected bool
}{
{"MANIFEST\t1", true},
{"version:\t10", true},
{"previous:\t0", true},
{"filecount:\t5", true},
{"timestamp:\t", true},
{"contentsize:\t", true},
{"includes:\tos-core", true},
{"10\t/foo", true},
{"\t0\t/foo", false},
{"10\t/usr/share", true},
{".d..\t", false},
}
// do not use the fileContains helper for this because we are checking a lot
// of strings in the same file
b, err := ioutil.ReadFile(filepath.Join(testDir, "www/10/Manifest.test-bundle"))
if err != nil {
t.Fatal("could not read test file for checking")
}
s := string(b)
for _, l := range lines {
if strings.Contains(s, l.sub) != l.expected {
if l.expected {
t.Errorf("'%s' not found in 10/Manifest.test-bundle", l.sub)
} else {
t.Errorf("invalid '%s' found in 10/Manifest.test-bundle", l.sub)
}
}
}
lines20 := []struct {
sub string
expected bool
}{
{"MANIFEST\t1", true},
{"version:\t20", true},
{"previous:\t10", true},
{"filecount:\t5", true},
{"includes:\tos-core", true},
{"20\t/foo", true},
{"10\t/foo", false},
}
// do not use the fileContains helper for this because we are checking a lot
// of strings in the same file
b, err = ioutil.ReadFile(filepath.Join(testDir, "www/20/Manifest.test-bundle"))
if err != nil {
t.Fatal("could not read test file for checking")
}
s = string(b)
for _, l := range lines20 {
if strings.Contains(s, l.sub) != l.expected {
if l.expected {
t.Errorf("'%s' not found in 20/Manifest.test-bundle", l.sub)
} else {
t.Errorf("invalid '%s' found in 20/Manifest.test-bundle", l.sub)
}
}
}
}
func TestCreateManifestsDeleteNoVerBump(t *testing.T) {
// init test dir
testDir := "./testdata/testdata-delete-no-version-bump"
if err := os.RemoveAll(testDir); err != nil {
t.Fatal("unable to remove " + testDir)
}
cmd := exec.Command("cp", "-a", testDir+".bak", testDir)
if err := cmd.Run(); err != nil {
t.Fatalf("unable to copy %s.bak to %s", testDir, testDir)
}
if err := CreateManifests(10, false, 1, testDir); err != nil {
t.Error(err)
}
ver := []byte("10\n")
if err := ioutil.WriteFile(filepath.Join(testDir, "image", "LAST_VER"), ver, 0755); err != nil {
t.Fatal(err)
}
if err := CreateManifests(20, false, 1, testDir); err != nil {
t.Error(err)
}
if !fileContains(filepath.Join(testDir, "www/10/Manifest.full"), "10\t/foo") {
t.Error("'10\t/foo' not found in 10/Manifest.full")
}
if !fileContains(filepath.Join(testDir, "www/20/Manifest.full"), "10\t/foo") {
t.Error("'10\t/foo' not found in 20/Manifest.full")
}
if fileContains(filepath.Join(testDir, "www/20/Manifest.full"), "20\t/foo") {
t.Error("invalid '20\t/foo' found in 20/Manifest.full")
}
}
func TestCreateManifestIllegalChar(t *testing.T) {
testDir := "./testdata/testdata-filename-blacklisted"
if err := os.RemoveAll(testDir); err != nil {
t.Fatal("unable to remove " + testDir)
}
cmd := exec.Command("cp", "-a", testDir+".bak", testDir)
if err := cmd.Run(); err != nil {
t.Fatalf("unable to copy %s.bak to %s", testDir, testDir)
}
if err := CreateManifests(10, false, 1, testDir); err != nil {
t.Error(err)
}
if fileContains(filepath.Join(testDir, "www/10/Manifest.os-core"), "semicolon;") {
t.Error("illegal filename 'semicolon;' not blacklisted")
}
}
func TestCreateManifestDebuginfo(t *testing.T) {
testDir := "./testdata/testdata-filename-debuginfo"
if err := os.RemoveAll(testDir); err != nil {
t.Fatal("unable to remove " + testDir)
}
cmd := exec.Command("cp", "-a", testDir+".bak", testDir)
if err := cmd.Run(); err != nil {
t.Fatalf("unable to copy %s.bak to %s", testDir, testDir)
}
if err := CreateManifests(10, false, 1, testDir); err != nil {
t.Error(err)
}
if fileContains(filepath.Join(testDir, "www/10/Manifest.test-bundle"), "/usr/lib/debug/foo") {
t.Error("debuginfo file '/usr/lib/debug/foo' not banned")
}
if fileContains(filepath.Join(testDir, "www/10/Manifest.test-bundle"), "/usr/src/debug/bar") {
t.Error("debuginfo file '/usr/src/debug/bar' not banned")
}
}
func TestCreateManifestFormatNoDecrement(t *testing.T) {
testDir := "./testdata/testdata-format-no-decrement"
if err := os.RemoveAll(testDir); err != nil {
t.Fatal("unable to remove " + testDir)
}
cmd := exec.Command("cp", "-a", "./testdata/testdata-format.bak", testDir)
if err := cmd.Run(); err != nil {
t.Fatalf("unable to copy %s.bak to %s", testDir, testDir)
}
cmd = exec.Command("cp", "./testdata/format-no-dec-server.ini", testDir)
if err := cmd.Run(); err != nil {
t.Fatalf("unable to copy server.ini to %s", testDir)
}
if err := CreateManifests(10, false, 2, testDir); err != nil {
t.Error(err)
}
ver := []byte("10\n")
if err := ioutil.WriteFile(filepath.Join(testDir, "image", "LAST_VER"), ver, 0755); err != nil {
t.Fatal(err)
}
if err := CreateManifests(20, true, 1, testDir); err == nil {
t.Error("CreateManifests successful when decrementing format")
}
}
func TestCreateManifestFormat(t *testing.T) {
testDir := "./testdata/testdata-format"
if err := os.RemoveAll(testDir); err != nil {
t.Fatal("unable to remove " + testDir)
}
cmd := exec.Command("cp", "-a", testDir+".bak", testDir)
if err := cmd.Run(); err != nil {
t.Fatalf("unable to copy %s.bak to %s", testDir, testDir)
}
if err := CreateManifests(10, false, 1, testDir); err != nil {
t.Error(err)
}
ver := []byte("10\n")
if err := ioutil.WriteFile(filepath.Join(testDir, "image", "LAST_VER"), ver, 0755); err != nil {
t.Fatal(err)
}
if err := CreateManifests(20, true, 2, testDir); err != nil {
t.Error(err)
}
}
func TestCreateManifestGhosted(t *testing.T) {
testDir := "./testdata/testdata-ghosting"
if err := os.RemoveAll(testDir); err != nil {
t.Fatal("unable to remove " + testDir)
}
cmd := exec.Command("cp", "-a", testDir+".bak", testDir)
if err := cmd.Run(); err != nil {
t.Fatalf("unable to copy %s.bak to %s", testDir, testDir)
}
if err := CreateManifests(10, false, 1, testDir); err != nil {
t.Error(err)
}
ver := []byte("10\n")
if err := ioutil.WriteFile(filepath.Join(testDir, "image", "LAST_VER"), ver, 0755); err != nil {
t.Fatal(err)
}
if err := CreateManifests(20, false, 1, testDir); err != nil {
t.Error(err)
}
ver = []byte("20\n")
if err := ioutil.WriteFile(filepath.Join(testDir, "image", "LAST_VER"), ver, 0755); err != nil {
t.Fatal(err)
}
if err := CreateManifests(30, false, 1, testDir); err != nil {
t.Error(err)
}
re := regexp.MustCompile("F\\.b\\.\t.*\t10\t/usr/lib/kernel/bar")
if fileContainsRe(filepath.Join(testDir, "www/10/Manifest.full"), re) == "" {
t.Errorf("%v not found in 10/Manifest.full", re.String())
}
re = regexp.MustCompile("\\.gb\\.\t.*\t20\t/usr/lib/kernel/bar")
if fileContainsRe(filepath.Join(testDir, "www/20/Manifest.full"), re) == "" {
t.Errorf("%v not found in 20/Manifest.full", re.String())
}
re = regexp.MustCompile("F\\.b\\.\t.*\t20\t/usr/lib/kernel/baz")
if fileContainsRe(filepath.Join(testDir, "www/20/Manifest.full"), re) == "" {
t.Errorf("%v not found in 20/Manifest.full", re.String())
}
if fileContains(filepath.Join(testDir, "www/30/Manifest.full"), "/usr/lib/kernel/bar") {
t.Error("/usr/lib/kernel/bar not cleaned up in 30/Manifest.full")
}
re = regexp.MustCompile("\\.gb\\.\t.*\t30\t/usr/lib/kernel/baz")
if fileContainsRe(filepath.Join(testDir, "www/30/Manifest.full"), re) == "" {
t.Errorf("%v not found in 30/Manifest.full", re.String())
}
}
+12 -3
View File
@@ -21,6 +21,11 @@ import (
"strings"
)
func filenameBlacklisted(fname string) bool {
illegalChars := ";&|*`/<>\\\"'"
return strings.ContainsAny(fname, illegalChars)
}
// createFileRecord creates a manifest File entry from a file
// this function sets the Name, Info, Type, and Hash fields
// the Version field is additionally set using the global toVersion variable
@@ -28,7 +33,11 @@ func (m *Manifest) createFileRecord(rootPath string, path string, fi os.FileInfo
var file *File
fname := strings.TrimPrefix(path, rootPath)
if fname == "" {
// do not add "/" to manifest
return nil
}
if filenameBlacklisted(filepath.Base(fname)) {
fmt.Fprintf(os.Stderr, "%s is a blacklisted file name\n", fname)
return nil
}
@@ -45,7 +54,8 @@ func (m *Manifest) createFileRecord(rootPath string, path string, fi os.FileInfo
case mode&os.ModeSymlink != 0:
file.Type = typeLink
default:
return fmt.Errorf("%v is an unsupported file type", file.Name)
fmt.Fprintf(os.Stderr, "%v is an unsupported file type\n", file.Name)
return nil
}
fh, err := Hashcalc(rootPath + file.Name)
@@ -56,7 +66,6 @@ func (m *Manifest) createFileRecord(rootPath string, path string, fi os.FileInfo
file.Hash = fh
m.Files = append(m.Files, file)
m.Header.FileCount++
m.Header.ContentSize += uint64(fi.Size())
return nil
+3
View File
@@ -106,6 +106,9 @@ func (f *File) setBootFromPathname() {
for _, path := range bootPaths {
if strings.HasPrefix(f.Name, path) {
f.Modifier = modifierBoot
if f.Status == statusDeleted {
f.Status = statusGhosted
}
return
}
}
+28 -8
View File
@@ -307,21 +307,15 @@ func writeManifestFileEntry(file *File, w *bufio.Writer) error {
func (m *Manifest) WriteManifestFile(path string) error {
var err error
var f *os.File
if f, err = os.Create(path); err != nil {
if f, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
return fmt.Errorf("could not create %v: %v", path, err)
}
// close before setting permissions
defer func() {
cerr := f.Close()
if err == nil {
err = cerr
}
cherr := os.Chmod(path, 0644)
if err == nil {
err = cherr
}
}()
w := bufio.NewWriter(f)
@@ -429,8 +423,16 @@ func (m *Manifest) filesAdded(oldManifest *Manifest) int {
func (m *Manifest) newDeleted(oldManifest *Manifest) int {
deleted := 0
for _, df := range oldManifest.Files {
if df.Status != statusDeleted && df.findFileNameInSlice(m.DeletedFiles) != nil {
if df.Status != statusDeleted && df.findFileNameInSlice(m.Files) == nil {
if df.Status == statusGhosted {
continue
}
df.Version = m.Header.Version
df.Status = statusDeleted
df.Modifier = modifierUnset
df.Type = typeUnset
m.Files = append(m.Files, df)
m.DeletedFiles = append(m.DeletedFiles, df)
deleted++
}
}
@@ -530,3 +532,21 @@ func (m *Manifest) subtractManifests(m2 *Manifest) {
m.subtractManifestFromManifest(mi)
}
}
func (m *Manifest) removeDebuginfo(d dbgConfig) {
for i, f := range m.Files {
if strings.HasPrefix(f.Name, d.src) && len(f.Name) > len(d.src) {
copy(m.Files[i:], m.Files[i+1:])
m.Files[len(m.Files)-1] = &File{}
m.Files = m.Files[:len(m.Files)-1]
continue
}
if strings.HasPrefix(f.Name, d.lib) && len(f.Name) > len(d.lib) {
copy(m.Files[i:], m.Files[i+1:])
m.Files[len(m.Files)-1] = &File{}
m.Files = m.Files[:len(m.Files)-1]
continue
}
}
}
+28 -9
View File
@@ -1,8 +1,11 @@
package swupd
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
@@ -271,21 +274,37 @@ func TestWriteManifestFile(t *testing.T) {
t.Fatal("ReadManifestFromFile did not add file entried to the file list")
}
f, err := ioutil.TempFile("testdata", "manifest.result")
if err != nil {
t.Fatal("unable to open file for write")
}
defer os.Remove(f.Name())
newpath := f.Name()
// do not use a tempfile here, we just need the unique name
newpath := "testdata/manifest.good.result"
defer os.Remove(newpath)
if err := m.WriteManifestFile(newpath); err != nil {
t.Error(err)
}
if err := os.Chmod(path, 0644); err != nil {
t.Fatal("unable to change file permissions for test")
}
fh1, _ := Hashcalc(path)
fh2, _ := Hashcalc(newpath)
if fh1 != fh2 {
t.Errorf("generated %v did not match read %v file", newpath, path)
t.Errorf("generated %v (%v) did not match read %v (%v) file", newpath, fh2, path, fh1)
// Print some debug information
cmd := exec.Command("diff", newpath, path)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
t.Fatal("diff failed")
}
fmt.Println("DIFF")
fmt.Println(out.String())
info1, _ := os.Stat(path)
info2, _ := os.Stat(newpath)
fmt.Println("FILE PERMISSIONS")
fmt.Printf("read: %v\n", info1.Mode())
fmt.Printf("gend: %v\n", info2.Mode())
}
}
@@ -477,7 +496,7 @@ func TestNewDeleted(t *testing.T) {
}
mNew := Manifest{
DeletedFiles: []*File{
Files: []*File{
{Name: "1"},
{Name: "2"},
{Name: "3"},
+9
View File
@@ -0,0 +1,9 @@
[Server]
emptydir=./testdata/testdata-format-no-decrement/empty
imagebase=./testdata/testdata-format-no-decrement/image
outputdir=./testdata/testdata-format-no-decrement/www
[Debuginfo]
banned=true
lib=/usr/lib/debug/
src=/usr/src/debug/
@@ -1 +0,0 @@
no-op content
@@ -1 +0,0 @@
no-op content
-9
View File
@@ -1,9 +0,0 @@
[Server]
emptydir=./testdata/test-web-dir/empty/
imagebase=./testdata/test-web-dir/image/
outputdir=./testdata/test-web-dir/www/
[Debuginfo]
banned=true
lib=/usr/lib/debug/
src=/usr/src/debug/
+9
View File
@@ -0,0 +1,9 @@
[Server]
emptydir=./testdata/testdata-basic/empty/
imagebase=./testdata/testdata-basic/image/
outputdir=./testdata/testdata-basic/www/
[Debuginfo]
banned=true
lib=/usr/lib/debug/
src=/usr/src/debug/
@@ -0,0 +1,9 @@
[os-core]
group=os-core
status=ACTIVE
[test-bundle1]
group=test-bundle1
status=ACTIVE
[test-bundle2]
group=test-bundle2
status=ACTIVE
@@ -0,0 +1 @@
VERSION_ID=10
@@ -0,0 +1 @@
foo
@@ -0,0 +1 @@
VERSION_ID=10
@@ -0,0 +1 @@
foo
@@ -0,0 +1 @@
VERSION_ID=10
@@ -0,0 +1 @@
VERSION_ID=20
@@ -0,0 +1 @@
foo
@@ -0,0 +1 @@
VERSION_ID=20
@@ -0,0 +1 @@
VERSION_ID=20
@@ -0,0 +1 @@
0
@@ -0,0 +1,9 @@
[Server]
emptydir=./testdata/testdata-delete-no-version-bump/empty
imagebase=./testdata/testdata-delete-no-version-bump/image
outputdir=./testdata/testdata-delete-no-version-bump/www/
[Debuginfo]
banned=true
lib=/usr/lib/debug/
src=/usr/src/debug/
@@ -0,0 +1,3 @@
[os-core]
group=os-core
status=ACTIVE
@@ -0,0 +1 @@
semicolon;
@@ -0,0 +1 @@
VERSION_ID=10
@@ -0,0 +1 @@
0
@@ -0,0 +1,9 @@
[Server]
emptydir=./testdata/testdata-filename-blacklisted/empty
imagebase=./testdata/testdata-filename-blacklisted/image
outputdir=./testdata/testdata-filename-blacklisted/www
[Debuginfo]
banned=true
lib=/usr/lib/debug/
src=/usr/src/debug/
@@ -0,0 +1,6 @@
[os-core]
group=os-core
status=ACTIVE
[test-bundle]
group=test-bundle
status=ACTIVE
@@ -0,0 +1 @@
VERSION_ID=10
@@ -0,0 +1 @@
/usr/bin/foobar
@@ -0,0 +1 @@
/usr/lib/debug/foo
@@ -0,0 +1 @@
VERSION_ID=10
@@ -0,0 +1 @@
/usr/src/debug/bar
@@ -0,0 +1 @@
0
@@ -0,0 +1,9 @@
[Server]
emptydir=./testdata/testdata-filename-debuginfo/empty
imagebase=./testdata/testdata-filename-debuginfo/image
outputdir=./testdata/testdata-filename-debuginfo/www
[Debuginfo]
banned=true
lib=/usr/lib/debug/
src=/usr/src/debug/
+3
View File
@@ -0,0 +1,3 @@
[os-core]
group=os-core
status=ACTIVE
@@ -0,0 +1 @@
bar
@@ -0,0 +1 @@
foo
@@ -0,0 +1 @@
VERSION_ID=10
@@ -0,0 +1 @@
baz
@@ -0,0 +1 @@
foo
@@ -0,0 +1 @@
VERSION_ID=20
+1
View File
@@ -0,0 +1 @@
10
+9
View File
@@ -0,0 +1,9 @@
[Server]
emptydir=./testdata/testdata-format/empty
imagebase=./testdata/testdata-format/image
outputdir=./testdata/testdata-format/www
[Debuginfo]
banned=true
lib=/usr/lib/debug/
src=/usr/src/debug/
+6
View File
@@ -0,0 +1,6 @@
[os-core]
group=os-core
status=ACTIVE
[test-bundle]
group=test-bundle
status=ACTIVE
@@ -0,0 +1 @@
VERSION_ID=10
@@ -0,0 +1 @@
testfile_contents
@@ -0,0 +1 @@
VERSION_ID=10
@@ -0,0 +1 @@
VERSION_ID=20
@@ -0,0 +1 @@
new_testfile_contents
@@ -0,0 +1 @@
VERSION_ID=20
@@ -0,0 +1 @@
VERSION_ID=30
@@ -0,0 +1 @@
VERSION_ID=30
+1
View File
@@ -0,0 +1 @@
0
+9
View File
@@ -0,0 +1,9 @@
[Server]
emptydir=./testdata/testdata-ghosting/empty
imagebase=./testdata/testdata-ghosting/image
outputdir=./testdata/testdata-ghosting/www
[Debuginfo]
banned=true
lib=/usr/lib/debug/
src=/usr/src/debug/