Repository: prometheus/procfs
Pull request #149 (2019-04-11 20:38:14)

use FS struct for proc and sys mount points in submodules
Based on discussion in #148, instead of removing the FS type completely, this PR moves the FS type to a shared internal package and exposes a more consistent API for each of the subpackages.

pgier: One drawback of this approach is that the affected subpackages now have a dependency on the main procfs and/or sysfs packages.  It might be better to add the FS type to the internal/util package and have the sub-modules use that instead of the FS from procfs and sysfs.
pgier: @mdlayher @discordianfish PTAL.  I've updated this PR to move the FS/string type to the internal/util package.  This allows the sub-packages bcache, blockdevice, nfs, and xfs to work without a dependency on the main procfs and sysfs packages, which was the original purpose of #136.  And I made changes to the procfs and sysfs packages to be consistent with the subpackages.
discordianfish: Great, I like that!
One suggestion though (and I'm open for different opinions on this): I would create a fs/ module and move the functions there instead of util/. Util packages are seen as anti-pattern and while I wouldn't avoid them at all cost, it seems reasonable to use a fs module for these functions.

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index bc40f5b..6484242 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -4,10 +4,10 @@ version: 2.1
 commands:
   default:
     steps:
-    - run: make style check_license vet test staticcheck
+    - run: make style check_license test lint
   no-test:
     steps:
-    - run: make style check_license vet staticcheck
+    - run: make style check_license lint
 
 jobs:
   test:
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..438ca92
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,6 @@
+# Run only staticcheck for now. Additional linters will be enabled one-by-one.
+linters:
+  enable:
+  - staticcheck
+  - govet
+  disable-all: true
diff --git a/Makefile.common b/Makefile.common
index 873964f..73052b3 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -72,14 +72,13 @@ endif
 PROMU_VERSION ?= 0.3.0
 PROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
 
-STATICCHECK :=
-# staticcheck only supports linux, freebsd, darwin and windows platforms on i386/amd64
+GOLANGCI_LINT :=
+GOLANGCI_LINT_VERSION ?= v1.16.0
+# golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
 # windows isn't included here because of the path separator being different.
-ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin))
+ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
 	ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386))
-		STATICCHECK := $(FIRST_GOPATH)/bin/staticcheck
-		STATICCHECK_VERSION ?= 2019.1
-		STATICCHECK_URL := https://github.com/dominikh/go-tools/releases/download/$(STATICCHECK_VERSION)/staticcheck_$(GOHOSTOS)_$(GOHOSTARCH)
+		GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint
 	endif
 endif
 
@@ -107,7 +106,7 @@ endif
 %: common-% ;
 
 .PHONY: common-all
-common-all: precheck style check_license staticcheck unused build test
+common-all: precheck style check_license lint unused build test
 
 .PHONY: common-style
 common-style:
@@ -159,21 +158,24 @@ common-vet:
 	@echo ">> vetting code"
 	GO111MODULE=$(GO111MODULE) $(GO) vet $(GOOPTS) $(pkgs)
 
-.PHONY: common-staticcheck
-common-staticcheck: $(STATICCHECK)
-ifdef STATICCHECK
-	@echo ">> running staticcheck"
-	chmod +x $(STATICCHECK)
+.PHONY: common-lint
+common-lint: $(GOLANGCI_LINT)
+ifdef GOLANGCI_LINT
+	@echo ">> running golangci-lint"
 ifdef GO111MODULE
 # 'go list' needs to be executed before staticcheck to prepopulate the modules cache.
 # Otherwise staticcheck might fail randomly for some reason not yet explained.
 	GO111MODULE=$(GO111MODULE) $(GO) list -e -compiled -test=true -export=false -deps=true -find=false -tags= -- ./... > /dev/null
-	GO111MODULE=$(GO111MODULE) $(STATICCHECK) -ignore "$(STATICCHECK_IGNORE)" $(pkgs)
+	GO111MODULE=$(GO111MODULE) $(GOLANGCI_LINT) run $(pkgs)
 else
-	$(STATICCHECK) -ignore "$(STATICCHECK_IGNORE)" $(pkgs)
+	$(GOLANGCI_LINT) run $(pkgs)
 endif
 endif
 
+# For backward-compatibility.
+.PHONY: common-staticcheck
+common-staticcheck: lint
+
 .PHONY: common-unused
 common-unused: $(GOVENDOR)
 ifdef GOVENDOR
@@ -241,10 +243,10 @@ proto:
 	@echo ">> generating code from proto files"
 	@./scripts/genproto.sh
 
-ifdef STATICCHECK
-$(STATICCHECK):
+ifdef GOLANGCI_LINT
+$(GOLANGCI_LINT):
 	mkdir -p $(FIRST_GOPATH)/bin
-	curl -s -L $(STATICCHECK_URL) > $(STATICCHECK)
+	curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION)
 endif
 
 ifdef GOVENDOR
diff --git a/bcache/get.go b/bcache/get.go
index 3f955fa..8d404bd 100644
--- a/bcache/get.go
+++ b/bcache/get.go
@@ -22,11 +22,32 @@ import (
 	"path/filepath"
 	"strconv"
 	"strings"
+
+	"github.com/prometheus/procfs/internal/fs"
 )
 
-// ReadStats retrieves bcache runtime statistics for each bcache.
-func ReadStats(sysfs string) ([]*Stats, error) {
-	matches, err := filepath.Glob(path.Join(sysfs, "fs/bcache/*-*"))
+// FS represents the pseudo-filesystem proc, which provides an interface to
+// kernel data structures.
+type FS struct {
+	sys *fs.FS
+}
+
+// NewFS returns a new Bcache using the given sys fs mount point. It will error
+// if the mount point can't be read.
+func NewFS(mountPoint string) (FS, error) {
+	if strings.TrimSpace(mountPoint) == "" {
+		mountPoint = fs.DefaultSysMountPoint
+	}
+	fs, err := fs.NewFS(mountPoint)
+	if err != nil {
+		return FS{}, err
+	}
+	return FS{&fs}, nil
+}
+
+// Stats retrieves bcache runtime statistics for each bcache.
+func (fs FS) Stats() ([]*Stats, error) {
+	matches, err := filepath.Glob(fs.sys.Path("fs/bcache/*-*"))
 	if err != nil {
 		return nil, err
 	}
diff --git a/blockdevice/stats.go b/blockdevice/stats.go
index 3131529..efaeb81 100644
--- a/blockdevice/stats.go
+++ b/blockdevice/stats.go
@@ -19,8 +19,9 @@ import (
 	"io"
 	"io/ioutil"
 	"os"
-	"path"
 	"strings"
+
+	"github.com/prometheus/procfs/internal/fs"
 )
 
 // Info contains identifying information for a block device such as a disk drive
@@ -87,10 +88,37 @@ const (
 	sysBlockStatFormat  = "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d"
 )
 
-// ReadProcDiskstats reads the diskstats file and returns
+// FS represents the pseudo-filesystems proc and sys, which provides an
+// interface to kernel data structures.
+type FS struct {
+	proc *fs.FS
+	sys  *fs.FS
+}
+
+// NewFS returns a new XFS mounted under the given mountPoint. It will error
+// if the mount point can't be read.
+func NewFS(procMountPoint string, sysMountPoint string) (FS, error) {
+	if strings.TrimSpace(procMountPoint) == "" {
+		procMountPoint = fs.DefaultProcMountPoint
+	}
+	procfs, err := fs.NewFS(procMountPoint)
+	if err != nil {
+		return FS{}, err
+	}
+	if strings.TrimSpace(sysMountPoint) == "" {
+		sysMountPoint = fs.DefaultSysMountPoint
+	}
+	sysfs, err := fs.NewFS(sysMountPoint)
+	if err != nil {
+		return FS{}, err
+	}
+	return FS{&procfs, &sysfs}, nil
+}
+
+// ProcDiskstats reads the diskstats file and returns
 // an array of Diskstats (one per line/device)
-func ReadProcDiskstats(procfs string) ([]Diskstats, error) {
-	file, err := os.Open(path.Join(procfs, procDiskstatsPath))
+func (fs FS) ProcDiskstats() ([]Diskstats, error) {
+	file, err := os.Open(fs.proc.Path(procDiskstatsPath))
 	if err != nil {
 		return nil, err
 	}
@@ -132,9 +160,9 @@ func ReadProcDiskstats(procfs string) ([]Diskstats, error) {
 	return diskstats, scanner.Err()
 }
 
-// ListSysBlockDevices lists the device names from /sys/block/<dev>
-func ListSysBlockDevices(sysfs string) ([]string, error) {
-	deviceDirs, err := ioutil.ReadDir(path.Join(sysfs, sysBlockPath))
+// SysBlockDevices lists the device names from /sys/block/<dev>
+func (fs FS) SysBlockDevices() ([]string, error) {
+	deviceDirs, err := ioutil.ReadDir(fs.sys.Path(sysBlockPath))
 	if err != nil {
 		return nil, err
 	}
@@ -147,12 +175,12 @@ func ListSysBlockDevices(sysfs string) ([]string, error) {
 	return devices, nil
 }
 
-// ReadSysBlockDeviceStat returns stats for the block device read from /sys/block/<device>/stat.
+// SysBlockDeviceStat returns stats for the block device read from /sys/block/<device>/stat.
 // The number of stats read will be 15 if the discard stats are available (kernel 4.18+)
 // and 11 if they are not available.
-func ReadSysBlockDeviceStat(sysfs string, device string) (IOStats, int, error) {
+func (fs FS) SysBlockDeviceStat(device string) (IOStats, int, error) {
 	stat := IOStats{}
-	bytes, err := ioutil.ReadFile(path.Join(sysfs, sysBlockPath, device, "stat"))
+	bytes, err := ioutil.ReadFile(fs.sys.Path(sysBlockPath, device, "stat"))
 	if err != nil {
 		return stat, 0, err
 	}
diff --git a/buddyinfo.go b/buddyinfo.go
index d3a8268..5cd22a8 100644
--- a/buddyinfo.go
+++ b/buddyinfo.go
@@ -43,7 +43,7 @@ func NewBuddyInfo() ([]BuddyInfo, error) {
 
 // NewBuddyInfo reads the buddyinfo statistics from the specified `proc` filesystem.
 func (fs FS) NewBuddyInfo() ([]BuddyInfo, error) {
-	file, err := os.Open(fs.Path("buddyinfo"))
+	file, err := os.Open(fs.proc.Path("buddyinfo"))
 	if err != nil {
 		return nil, err
 	}
diff --git a/fs.go b/fs.go
index f7a151c..9c56c83 100644
--- a/fs.go
+++ b/fs.go
@@ -14,33 +14,24 @@
 package procfs
 
 import (
-	"fmt"
-	"os"
-	"path"
+	"github.com/prometheus/procfs/internal/fs"
 )
 
-// FS represents the pseudo-filesystem proc, which provides an interface to
+// FS represents the pseudo-filesystem sys, which provides an interface to
 // kernel data structures.
-type FS string
+type FS struct {
+	proc fs.FS
+}
 
 // DefaultMountPoint is the common mount point of the proc filesystem.
-const DefaultMountPoint = "/proc"
+const DefaultMountPoint = fs.DefaultProcMountPoint
 
-// NewFS returns a new FS mounted under the given mountPoint. It will error
-// if the mount point can't be read.
+// NewFS returns a new proc FS mounted under the given proc mountPoint. It will error
+// if the mount point dirctory can't be read or is a file.
 func NewFS(mountPoint string) (FS, error) {
-	info, err := os.Stat(mountPoint)
+	fs, err := fs.NewFS(mountPoint)
 	if err != nil {
-		return "", fmt.Errorf("could not read %s: %s", mountPoint, err)
-	}
-	if !info.IsDir() {
-		return "", fmt.Errorf("mount point %s is not a directory", mountPoint)
+		return FS{}, err
 	}
-
-	return FS(mountPoint), nil
-}
-
-// Path returns the path of the given subsystem relative to the procfs root.
-func (fs FS) Path(p ...string) string {
-	return path.Join(append([]string{string(fs)}, p...)...)
+	return FS{fs}, nil
 }
diff --git a/internal/fs/fs.go b/internal/fs/fs.go
new file mode 100644
index 0000000..c66a1cf
--- /dev/null
+++ b/internal/fs/fs.go
@@ -0,0 +1,52 @@
+// Copyright 2019 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fs
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+)
+
+const (
+	// DefaultProcMountPoint is the common mount point of the proc filesystem.
+	DefaultProcMountPoint = "/proc"
+
+	// DefaultSysMountPoint is the common mount point of the sys filesystem.
+	DefaultSysMountPoint = "/sys"
+)
+
+// FS represents a pseudo-filesystem, normally /proc or /sys, which provides an
+// interface to kernel data structures.
+type FS string
+
+// NewFS returns a new FS mounted under the given mountPoint. It will error
+// if the mount point can't be read.
+func NewFS(mountPoint string) (FS, error) {
+	info, err := os.Stat(mountPoint)
+	if err != nil {
+		return "", fmt.Errorf("could not read %s: %s", mountPoint, err)
+	}
+	if !info.IsDir() {
+		return "", fmt.Errorf("mount point %s is not a directory", mountPoint)
+	}
+
+	return FS(mountPoint), nil
+}
+
+// Path appends the given path elements to the filesystem path, adding separators
+// as necessary.
+func (fs FS) Path(p ...string) string {
+	return filepath.Join(append([]string{string(fs)}, p...)...)
+}
diff --git a/ipvs.go b/ipvs.go
index e36d4a3..41e645d 100644
--- a/ipvs.go
+++ b/ipvs.go
@@ -74,7 +74,7 @@ func NewIPVSStats() (IPVSStats, error) {
 
 // NewIPVSStats reads the IPVS statistics from the specified `proc` filesystem.
 func (fs FS) NewIPVSStats() (IPVSStats, error) {
-	file, err := os.Open(fs.Path("net/ip_vs_stats"))
+	file, err := os.Open(fs.proc.Path("net/ip_vs_stats"))
 	if err != nil {
 		return IPVSStats{}, err
 	}
@@ -143,7 +143,7 @@ func NewIPVSBackendStatus() ([]IPVSBackendStatus, error) {
 
 // NewIPVSBackendStatus reads and returns the status of all (virtual,real) server pairs from the specified `proc` filesystem.
 func (fs FS) NewIPVSBackendStatus() ([]IPVSBackendStatus, error) {
-	file, err := os.Open(fs.Path("net/ip_vs"))
+	file, err := os.Open(fs.proc.Path("net/ip_vs"))
 	if err != nil {
 		return nil, err
 	}
diff --git a/mdstat.go b/mdstat.go
index 9dc1958..6ac7a12 100644
--- a/mdstat.go
+++ b/mdstat.go
@@ -44,7 +44,7 @@ type MDStat struct {
 
 // ParseMDStat parses an mdstat-file and returns a struct with the relevant infos.
 func (fs FS) ParseMDStat() (mdstates []MDStat, err error) {
-	mdStatusFilePath := fs.Path("mdstat")
+	mdStatusFilePath := fs.proc.Path("mdstat")
 	content, err := ioutil.ReadFile(mdStatusFilePath)
 	if err != nil {
 		return []MDStat{}, fmt.Errorf("error parsing %s: %s", mdStatusFilePath, err)
diff --git a/net_dev.go b/net_dev.go
index 3f25233..0063594 100644
--- a/net_dev.go
+++ b/net_dev.go
@@ -59,7 +59,7 @@ func NewNetDev() (NetDev, error) {
 
 // NewNetDev returns kernel/system statistics read from /proc/net/dev.
 func (fs FS) NewNetDev() (NetDev, error) {
-	return newNetDev(fs.Path("net/dev"))
+	return newNetDev(fs.proc.Path("net/dev"))
 }
 
 // NewNetDev returns kernel/system statistics read from /proc/[pid]/net/dev.
diff --git a/nfs/nfs.go b/nfs/nfs.go
index 4c8dad4..7ee262f 100644
--- a/nfs/nfs.go
+++ b/nfs/nfs.go
@@ -17,7 +17,9 @@ package nfs
 
 import (
 	"os"
-	"path"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/fs"
 )
 
 // ReplyCache models the "rc" line.
@@ -267,10 +269,29 @@ type ServerRPCStats struct {
 	V4Ops          V4Ops
 }
 
-// ReadClientRPCStats retrieves NFS client RPC statistics
+// FS represents the pseudo-filesystem proc, which provides an interface to
+// kernel data structures.
+type FS struct {
+	proc *fs.FS
+}
+
+// NewFS returns a new FS mounted under the given mountPoint. It will error
+// if the mount point can't be read.
+func NewFS(mountPoint string) (FS, error) {
+	if strings.TrimSpace(mountPoint) == "" {
+		mountPoint = fs.DefaultProcMountPoint
+	}
+	fs, err := fs.NewFS(mountPoint)
+	if err != nil {
+		return FS{}, err
+	}
+	return FS{&fs}, nil
+}
+
+// ClientRPCStats retrieves NFS client RPC statistics
 // from proc/net/rpc/nfs.
-func ReadClientRPCStats(procfs string) (*ClientRPCStats, error) {
-	f, err := os.Open(path.Join(procfs, "net/rpc/nfs"))
+func (fs FS) ClientRPCStats() (*ClientRPCStats, error) {
+	f, err := os.Open(fs.proc.Path("net/rpc/nfs"))
 	if err != nil {
 		return nil, err
 	}
@@ -279,10 +300,10 @@ func ReadClientRPCStats(procfs string) (*ClientRPCStats, error) {
 	return ParseClientRPCStats(f)
 }
 
-// ReadServerRPCStats retrieves NFS daemon RPC statistics
+// ServerRPCStats retrieves NFS daemon RPC statistics
 // from proc/net/rpc/nfsd.
-func ReadServerRPCStats(procfs string) (*ServerRPCStats, error) {
-	f, err := os.Open(path.Join(procfs, "net/rpc/nfsd"))
+func (fs FS) ServerRPCStats() (*ServerRPCStats, error) {
+	f, err := os.Open(fs.proc.Path("net/rpc/nfsd"))
 	if err != nil {
 		return nil, err
 	}
diff --git a/proc.go b/proc.go
index 06bed0e..8e38493 100644
--- a/proc.go
+++ b/proc.go
@@ -20,6 +20,8 @@ import (
 	"os"
 	"strconv"
 	"strings"
+
+	"github.com/prometheus/procfs/internal/fs"
 )
 
 // Proc provides information about a running process.
@@ -27,7 +29,7 @@ type Proc struct {
 	// The process ID.
 	PID int
 
-	fs FS
+	fs fs.FS
 }
 
 // Procs represents a list of Proc structs.
@@ -66,11 +68,11 @@ func AllProcs() (Procs, error) {
 
 // Self returns a process for the current process.
 func (fs FS) Self() (Proc, error) {
-	p, err := os.Readlink(fs.Path("self"))
+	p, err := os.Readlink(fs.proc.Path("self"))
 	if err != nil {
 		return Proc{}, err
 	}
-	pid, err := strconv.Atoi(strings.Replace(p, string(fs), "", -1))
+	pid, err := strconv.Atoi(strings.Replace(p, string(fs.proc), "", -1))
 	if err != nil {
 		return Proc{}, err
 	}
@@ -79,15 +81,15 @@ func (fs FS) Self() (Proc, error) {
 
 // NewProc returns a process for the given pid.
 func (fs FS) NewProc(pid int) (Proc, error) {
-	if _, err := os.Stat(fs.Path(strconv.Itoa(pid))); err != nil {
+	if _, err := os.Stat(fs.proc.Path(strconv.Itoa(pid))); err != nil {
 		return Proc{}, err
 	}
-	return Proc{PID: pid, fs: fs}, nil
+	return Proc{PID: pid, fs: fs.proc}, nil
 }
 
 // AllProcs returns a list of all currently available processes.
 func (fs FS) AllProcs() (Procs, error) {
-	d, err := os.Open(fs.Path())
+	d, err := os.Open(fs.proc.Path())
 	if err != nil {
 		return Procs{}, err
 	}
@@ -104,7 +106,7 @@ func (fs FS) AllProcs() (Procs, error) {
 		if err != nil {
 			continue
 		}
-		p = append(p, Proc{PID: int(pid), fs: fs})
+		p = append(p, Proc{PID: int(pid), fs: fs.proc})
 	}
 
 	return p, nil
diff --git a/proc_psi.go b/proc_psi.go
index 4f11cdb..a23d4c0 100644
--- a/proc_psi.go
+++ b/proc_psi.go
@@ -64,7 +64,7 @@ func NewPSIStatsForResource(resource string) (PSIStats, error) {
 
 // NewPSIStatsForResource reads pressure stall information from /proc/pressure/<resource>
 func (fs FS) NewPSIStatsForResource(resource string) (PSIStats, error) {
-	file, err := os.Open(fs.Path(fmt.Sprintf("%s/%s", "pressure", resource)))
+	file, err := os.Open(fs.proc.Path(fmt.Sprintf("%s/%s", "pressure", resource)))
 	if err != nil {
 		return PSIStats{}, fmt.Errorf("psi_stats: unavailable for %s", resource)
 	}
diff --git a/proc_stat.go b/proc_stat.go
index e7c626a..4c8b03c 100644
--- a/proc_stat.go
+++ b/proc_stat.go
@@ -18,6 +18,8 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+
+	"github.com/prometheus/procfs/internal/fs"
 )
 
 // Originally, this USER_HZ value was dynamically retrieved via a sysconf call
@@ -99,7 +101,7 @@ type ProcStat struct {
 	// Resident set size in pages.
 	RSS int
 
-	fs FS
+	proc fs.FS
 }
 
 // NewStat returns the current status information of the process.
@@ -118,7 +120,7 @@ func (p Proc) NewStat() (ProcStat, error) {
 	var (
 		ignore int
 
-		s = ProcStat{PID: p.PID, fs: p.fs}
+		s = ProcStat{PID: p.PID, proc: p.fs}
 		l = bytes.Index(data, []byte("("))
 		r = bytes.LastIndex(data, []byte(")"))
 	)
@@ -175,7 +177,8 @@ func (s ProcStat) ResidentMemory() int {
 
 // StartTime returns the unix timestamp of the process in seconds.
 func (s ProcStat) StartTime() (float64, error) {
-	stat, err := s.fs.NewStat()
+	fs := FS{proc: s.proc}
+	stat, err := fs.NewStat()
 	if err != nil {
 		return 0, err
 	}
diff --git a/stat.go b/stat.go
index 61eb6b0..44c9af1 100644
--- a/stat.go
+++ b/stat.go
@@ -153,7 +153,7 @@ func parseSoftIRQStat(line string) (SoftIRQStat, uint64, error) {
 func (fs FS) NewStat() (Stat, error) {
 	// See https://www.kernel.org/doc/Documentation/filesystems/proc.txt
 
-	f, err := os.Open(fs.Path("stat"))
+	f, err := os.Open(fs.proc.Path("stat"))
 	if err != nil {
 		return Stat{}, err
 	}
diff --git a/sysfs/class_power_supply.go b/sysfs/class_power_supply.go
index e0b65cc..64720ef 100644
--- a/sysfs/class_power_supply.go
+++ b/sysfs/class_power_supply.go
@@ -114,7 +114,7 @@ func NewPowerSupplyClass() (PowerSupplyClass, error) {
 
 // NewPowerSupplyClass returns info for all power supplies read from /sys/class/power_supply/.
 func (fs FS) NewPowerSupplyClass() (PowerSupplyClass, error) {
-	path := fs.Path("class/power_supply")
+	path := fs.sys.Path("class/power_supply")
 
 	powerSupplyDirs, err := ioutil.ReadDir(path)
 	if err != nil {
diff --git a/sysfs/class_thermal.go b/sysfs/class_thermal.go
index 4f9cb9a..2d70795 100644
--- a/sysfs/class_thermal.go
+++ b/sysfs/class_thermal.go
@@ -37,7 +37,7 @@ type ClassThermalZoneStats struct {
 
 // NewClassThermalZoneStats returns Thermal Zone metrics for all zones.
 func (fs FS) NewClassThermalZoneStats() ([]ClassThermalZoneStats, error) {
-	zones, err := filepath.Glob(fs.Path("class/thermal/thermal_zone[0-9]*"))
+	zones, err := filepath.Glob(fs.sys.Path("class/thermal/thermal_zone[0-9]*"))
 	if err != nil {
 		return []ClassThermalZoneStats{}, err
 	}
diff --git a/sysfs/fs.go b/sysfs/fs.go
index 8dd258d..c0cb9b6 100644
--- a/sysfs/fs.go
+++ b/sysfs/fs.go
@@ -14,33 +14,24 @@
 package sysfs
 
 import (
-	"fmt"
-	"os"
-	"path/filepath"
+	"github.com/prometheus/procfs/internal/fs"
 )
 
 // FS represents the pseudo-filesystem sys, which provides an interface to
 // kernel data structures.
-type FS string
+type FS struct {
+	sys fs.FS
+}
 
 // DefaultMountPoint is the common mount point of the sys filesystem.
-const DefaultMountPoint = "/sys"
+const DefaultMountPoint = fs.DefaultSysMountPoint
 
 // NewFS returns a new FS mounted under the given mountPoint. It will error
 // if the mount point can't be read.
 func NewFS(mountPoint string) (FS, error) {
-	info, err := os.Stat(mountPoint)
+	fs, err := fs.NewFS(mountPoint)
 	if err != nil {
-		return "", fmt.Errorf("could not read %s: %s", mountPoint, err)
-	}
-	if !info.IsDir() {
-		return "", fmt.Errorf("mount point %s is not a directory", mountPoint)
+		return FS{}, err
 	}
-
-	return FS(mountPoint), nil
-}
-
-// Path returns the path of the given subsystem relative to the sys root.
-func (fs FS) Path(p ...string) string {
-	return filepath.Join(append([]string{string(fs)}, p...)...)
+	return FS{fs}, nil
 }
diff --git a/sysfs/net_class.go b/sysfs/net_class.go
index 5de445f..c28051e 100644
--- a/sysfs/net_class.go
+++ b/sysfs/net_class.go
@@ -78,7 +78,7 @@ func NewNetClass() (NetClass, error) {
 // NetClassDevices scans /sys/class/net for devices and returns them as a list of names.
 func (fs FS) NetClassDevices() ([]string, error) {
 	var res []string
-	path := fs.Path(netclassPath)
+	path := fs.sys.Path(netclassPath)
 
 	devices, err := ioutil.ReadDir(path)
 	if err != nil {
@@ -102,7 +102,7 @@ func (fs FS) NewNetClass() (NetClass, error) {
 		return nil, err
 	}
 
-	path := fs.Path(netclassPath)
+	path := fs.sys.Path(netclassPath)
 	netClass := NetClass{}
 	for _, deviceDir := range devices {
 		interfaceClass, err := netClass.parseNetClassIface(filepath.Join(path, deviceDir))
diff --git a/sysfs/system_cpu.go b/sysfs/system_cpu.go
index 6f48aa7..4bcf86b 100644
--- a/sysfs/system_cpu.go
+++ b/sysfs/system_cpu.go
@@ -60,7 +60,7 @@ func NewSystemCpufreq() ([]SystemCPUCpufreqStats, error) {
 func (fs FS) NewSystemCpufreq() ([]SystemCPUCpufreqStats, error) {
 	var g errgroup.Group
 
-	cpus, err := filepath.Glob(fs.Path("devices/system/cpu/cpu[0-9]*"))
+	cpus, err := filepath.Glob(fs.sys.Path("devices/system/cpu/cpu[0-9]*"))
 	if err != nil {
 		return nil, err
 	}
@@ -70,10 +70,10 @@ func (fs FS) NewSystemCpufreq() ([]SystemCPUCpufreqStats, error) {
 		cpuName := strings.TrimPrefix(filepath.Base(cpu), "cpu")
 
 		cpuCpufreqPath := filepath.Join(cpu, "cpufreq")
-		if _, err := os.Stat(cpuCpufreqPath); os.IsNotExist(err) {
+		_, err = os.Stat(cpuCpufreqPath)
+		if os.IsNotExist(err) {
 			continue
-		}
-		if err != nil {
+		} else if err != nil {
 			return nil, err
 		}
 
diff --git a/xfrm.go b/xfrm.go
index 8f1508f..30aa417 100644
--- a/xfrm.go
+++ b/xfrm.go
@@ -97,7 +97,7 @@ func NewXfrmStat() (XfrmStat, error) {
 
 // NewXfrmStat reads the xfrm_stat statistics from the 'proc' filesystem.
 func (fs FS) NewXfrmStat() (XfrmStat, error) {
-	file, err := os.Open(fs.Path("net/xfrm_stat"))
+	file, err := os.Open(fs.proc.Path("net/xfrm_stat"))
 	if err != nil {
 		return XfrmStat{}, err
 	}
diff --git a/xfs/xfs.go b/xfs/xfs.go
index 4bb793f..8a7288f 100644
--- a/xfs/xfs.go
+++ b/xfs/xfs.go
@@ -16,8 +16,10 @@ package xfs
 
 import (
 	"os"
-	"path"
 	"path/filepath"
+	"strings"
+
+	"github.com/prometheus/procfs/internal/fs"
 )
 
 // Stats contains XFS filesystem runtime statistics, parsed from
@@ -168,10 +170,37 @@ type ExtendedPrecisionStats struct {
 	ReadBytes  uint64
 }
 
-// ReadProcStat retrieves XFS filesystem runtime statistics
+// FS represents the pseudo-filesystems proc and sys, which provides an interface to
+// kernel data structures.
+type FS struct {
+	proc *fs.FS
+	sys  *fs.FS
+}
+
+// NewFS returns a new XFS handle using the given proc and sys mountPoints. It will error
+// if either of the mounts point can't be read.
+func NewFS(procMountPoint string, sysMountPoint string) (FS, error) {
+	if strings.TrimSpace(procMountPoint) == "" {
+		procMountPoint = fs.DefaultProcMountPoint
+	}
+	procfs, err := fs.NewFS(procMountPoint)
+	if err != nil {
+		return FS{}, err
+	}
+	if strings.TrimSpace(sysMountPoint) == "" {
+		sysMountPoint = fs.DefaultSysMountPoint
+	}
+	sysfs, err := fs.NewFS(sysMountPoint)
+	if err != nil {
+		return FS{}, err
+	}
+	return FS{&procfs, &sysfs}, nil
+}
+
+// ProcStat retrieves XFS filesystem runtime statistics
 // from proc/fs/xfs/stat given the profs mount point.
-func ReadProcStat(procfs string) (*Stats, error) {
-	f, err := os.Open(path.Join(procfs, "fs/xfs/stat"))
+func (fs FS) ProcStat() (*Stats, error) {
+	f, err := os.Open(fs.proc.Path("fs/xfs/stat"))
 	if err != nil {
 		return nil, err
 	}
@@ -180,11 +209,11 @@ func ReadProcStat(procfs string) (*Stats, error) {
 	return ParseStats(f)
 }
 
-// ReadSysStats retrieves XFS filesystem runtime statistics for each mounted XFS
+// SysStats retrieves XFS filesystem runtime statistics for each mounted XFS
 // filesystem.  Only available on kernel 4.4+.  On older kernels, an empty
 // slice of *xfs.Stats will be returned.
-func ReadSysStats(sysfs string) ([]*Stats, error) {
-	matches, err := filepath.Glob(path.Join(sysfs, "fs/xfs/*/stats/stats"))
+func (fs FS) SysStats() ([]*Stats, error) {
+	matches, err := filepath.Glob(fs.sys.Path("fs/xfs/*/stats/stats"))
 	if err != nil {
 		return nil, err
 	}


Test patch:
diff --git a/bcache/get_test.go b/bcache/get_test.go
index 5bea2f7..e448797 100644
--- a/bcache/get_test.go
+++ b/bcache/get_test.go
@@ -19,7 +19,11 @@ import (
 )
 
 func TestFSBcacheStats(t *testing.T) {
-	stats, err := ReadStats("../fixtures/sys")
+	bcache, err := NewFS("../fixtures/sys")
+	if err != nil {
+		t.Fatalf("failed to access bcache fs: %v", err)
+	}
+	stats, err := bcache.Stats()
 	if err != nil {
 		t.Fatalf("failed to parse bcache stats: %v", err)
 	}
diff --git a/blockdevice/stats_test.go b/blockdevice/stats_test.go
index e5a7b8c..2fea2c8 100644
--- a/blockdevice/stats_test.go
+++ b/blockdevice/stats_test.go
@@ -24,7 +24,11 @@ const (
 )
 
 func TestDiskstats(t *testing.T) {
-	diskstats, err := ReadProcDiskstats(procfsFixtures)
+	blockdevice, err := NewFS(procfsFixtures, sysfsFixtures)
+	if err != nil {
+		t.Fatalf("failed to access blockdevice fs: %v", err)
+	}
+	diskstats, err := blockdevice.ProcDiskstats()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -50,7 +54,11 @@ func TestDiskstats(t *testing.T) {
 }
 
 func TestBlockDevice(t *testing.T) {
-	devices, err := ListSysBlockDevices(sysfsFixtures)
+	blockdevice, err := NewFS("../fixtures/proc", "../fixtures/sys")
+	if err != nil {
+		t.Fatalf("failed to access blockdevice fs: %v", err)
+	}
+	devices, err := blockdevice.SysBlockDevices()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -61,7 +69,7 @@ func TestBlockDevice(t *testing.T) {
 	if devices[0] != "dm-0" {
 		t.Errorf(failMsgFormat, "Incorrect device name", "dm-0", devices[0])
 	}
-	device0stats, count, err := ReadSysBlockDeviceStat(sysfsFixtures, devices[0])
+	device0stats, count, err := blockdevice.SysBlockDeviceStat(devices[0])
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -74,7 +82,7 @@ func TestBlockDevice(t *testing.T) {
 	if device0stats.WeightedIOTicks != 6088971 {
 		t.Errorf(failMsgFormat, "Incorrect time in queue", 6088971, device0stats.WeightedIOTicks)
 	}
-	device1stats, count, err := ReadSysBlockDeviceStat(sysfsFixtures, devices[1])
+	device1stats, count, err := blockdevice.SysBlockDeviceStat(devices[1])
 	if count != 15 {
 		t.Errorf(failMsgFormat, "Incorrect number of stats read", 15, count)
 	}
diff --git a/buddyinfo_test.go b/buddyinfo_test.go
index 9aee650..7016466 100644
--- a/buddyinfo_test.go
+++ b/buddyinfo_test.go
@@ -19,7 +19,7 @@ import (
 )
 
 func TestBuddyInfo(t *testing.T) {
-	buddyInfo, err := FS("fixtures/proc/").NewBuddyInfo()
+	buddyInfo, err := getProcFixtures(t).NewBuddyInfo()
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/fs_test.go b/fs_test.go
index ce16fae..6e57745 100644
--- a/fs_test.go
+++ b/fs_test.go
@@ -27,4 +27,13 @@ func TestNewFS(t *testing.T) {
 	if _, err := NewFS("procfs.go"); err == nil {
 		t.Error("want NewFS to fail if mount point is not a directory")
 	}
+	getProcFixtures(t)
+}
+
+func getProcFixtures(t *testing.T) FS {
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return fs
 }
diff --git a/internal/fs/fs_test.go b/internal/fs/fs_test.go
new file mode 100644
index 0000000..bebcbe2
--- /dev/null
+++ b/internal/fs/fs_test.go
@@ -0,0 +1,34 @@
+// Copyright 2019 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fs
+
+import "testing"
+
+const (
+	sysTestFixtures = "../../fixtures/sys"
+)
+
+func TestNewFS(t *testing.T) {
+	if _, err := NewFS("foobar"); err == nil {
+		t.Error("want NewFS to fail for non-existing mount point")
+	}
+
+	if _, err := NewFS("doc.go"); err == nil {
+		t.Error("want NewFS to fail if mount point is not a directory")
+	}
+
+	if _, err := NewFS(sysTestFixtures); err != nil {
+		t.Error("want NewFS to succeed if mount point exists")
+	}
+}
diff --git a/ipvs_test.go b/ipvs_test.go
index e84bda5..aa87d3c 100644
--- a/ipvs_test.go
+++ b/ipvs_test.go
@@ -159,7 +159,11 @@ var (
 )
 
 func TestIPVSStats(t *testing.T) {
-	stats, err := FS(procTestFixtures).NewIPVSStats()
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		t.Fatal(err)
+	}
+	stats, err := fs.NewIPVSStats()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -213,7 +217,7 @@ func TestParseIPPortIPv6(t *testing.T) {
 }
 
 func TestIPVSBackendStatus(t *testing.T) {
-	backendStats, err := FS(procTestFixtures).NewIPVSBackendStatus()
+	backendStats, err := getProcFixtures(t).NewIPVSBackendStatus()
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/mdstat_test.go b/mdstat_test.go
index df92597..cc1b557 100644
--- a/mdstat_test.go
+++ b/mdstat_test.go
@@ -18,7 +18,7 @@ import (
 )
 
 func TestMDStat(t *testing.T) {
-	mdStates, err := FS(procTestFixtures).ParseMDStat()
+	mdStates, err := getProcFixtures(t).ParseMDStat()
 	if err != nil {
 		t.Fatalf("parsing of reference-file failed entirely: %s", err)
 	}
diff --git a/mountstats_test.go b/mountstats_test.go
index bc47a6e..727af07 100644
--- a/mountstats_test.go
+++ b/mountstats_test.go
@@ -344,7 +344,7 @@ func TestMountStats(t *testing.T) {
 		if tt.s != "" {
 			mounts, err = parseMountStats(strings.NewReader(tt.s))
 		} else {
-			proc, e := FS(procTestFixtures).NewProc(26231)
+			proc, e := getProcFixtures(t).NewProc(26231)
 			if e != nil {
 				t.Fatalf("failed to create proc: %v", err)
 			}
diff --git a/net_dev_test.go b/net_dev_test.go
index 5165eea..363c194 100644
--- a/net_dev_test.go
+++ b/net_dev_test.go
@@ -60,7 +60,7 @@ func TestNewNetDev(t *testing.T) {
 }
 
 func TestProcNewNetDev(t *testing.T) {
-	p, err := FS(procTestFixtures).NewProc(26231)
+	p, err := getProcFixtures(t).NewProc(26231)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/proc_io_test.go b/proc_io_test.go
index 1b0bc06..c0d55cd 100644
--- a/proc_io_test.go
+++ b/proc_io_test.go
@@ -16,7 +16,7 @@ package procfs
 import "testing"
 
 func TestProcIO(t *testing.T) {
-	p, err := FS(procTestFixtures).NewProc(26231)
+	p, err := getProcFixtures(t).NewProc(26231)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/proc_limits_test.go b/proc_limits_test.go
index f696c5f..290f675 100644
--- a/proc_limits_test.go
+++ b/proc_limits_test.go
@@ -16,7 +16,7 @@ package procfs
 import "testing"
 
 func TestNewLimits(t *testing.T) {
-	p, err := FS(procTestFixtures).NewProc(26231)
+	p, err := getProcFixtures(t).NewProc(26231)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/proc_ns_test.go b/proc_ns_test.go
index ce00276..ef62266 100644
--- a/proc_ns_test.go
+++ b/proc_ns_test.go
@@ -18,7 +18,7 @@ import (
 )
 
 func TestNewNamespaces(t *testing.T) {
-	p, err := FS(procTestFixtures).NewProc(26231)
+	p, err := getProcFixtures(t).NewProc(26231)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/proc_psi_test.go b/proc_psi_test.go
index 6ee2876..d238fe1 100644
--- a/proc_psi_test.go
+++ b/proc_psi_test.go
@@ -20,7 +20,7 @@ import (
 
 func TestPSIStats(t *testing.T) {
 	t.Run("fake", func(*testing.T) {
-		stats, err := FS(procTestFixtures).NewPSIStatsForResource("fake")
+		stats, err := getProcFixtures(t).NewPSIStatsForResource("fake")
 		if err == nil {
 			t.Fatal("fake resource does not have PSI statistics")
 		}
@@ -31,7 +31,7 @@ func TestPSIStats(t *testing.T) {
 	})
 
 	t.Run("cpu", func(t *testing.T) {
-		stats, err := FS(procTestFixtures).NewPSIStatsForResource("cpu")
+		stats, err := getProcFixtures(t).NewPSIStatsForResource("cpu")
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -68,7 +68,7 @@ func TestPSIStats(t *testing.T) {
 
 	for _, resource := range res {
 		t.Run(resource, func(t *testing.T) {
-			stats, err := FS(procTestFixtures).NewPSIStatsForResource(resource)
+			stats, err := getProcFixtures(t).NewPSIStatsForResource(resource)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/proc_stat_test.go b/proc_stat_test.go
index ea4023e..3822f55 100644
--- a/proc_stat_test.go
+++ b/proc_stat_test.go
@@ -19,7 +19,7 @@ import (
 )
 
 func TestProcStat(t *testing.T) {
-	p, err := FS(procTestFixtures).NewProc(26231)
+	p, err := getProcFixtures(t).NewProc(26231)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -114,7 +114,11 @@ func TestProcStatCPUTime(t *testing.T) {
 }
 
 func testProcStat(pid int) (ProcStat, error) {
-	p, err := FS(procTestFixtures).NewProc(pid)
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		return ProcStat{}, err
+	}
+	p, err := fs.NewProc(pid)
 	if err != nil {
 		return ProcStat{}, err
 	}
diff --git a/proc_test.go b/proc_test.go
index 1d3e6a3..f4da574 100644
--- a/proc_test.go
+++ b/proc_test.go
@@ -20,7 +20,7 @@ import (
 )
 
 func TestSelf(t *testing.T) {
-	fs := FS(procTestFixtures)
+	fs := getProcFixtures(t)
 
 	p1, err := fs.NewProc(26231)
 	if err != nil {
@@ -37,7 +37,7 @@ func TestSelf(t *testing.T) {
 }
 
 func TestAllProcs(t *testing.T) {
-	procs, err := FS(procTestFixtures).AllProcs()
+	procs, err := getProcFixtures(t).AllProcs()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -58,7 +58,7 @@ func TestCmdLine(t *testing.T) {
 		{process: 26232, want: []string{}},
 		{process: 26233, want: []string{"com.github.uiautomator"}},
 	} {
-		p1, err := FS(procTestFixtures).NewProc(tt.process)
+		p1, err := getProcFixtures(t).NewProc(tt.process)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -80,7 +80,7 @@ func TestComm(t *testing.T) {
 		{process: 26231, want: "vim"},
 		{process: 26232, want: "ata_sff"},
 	} {
-		p1, err := FS(procTestFixtures).NewProc(tt.process)
+		p1, err := getProcFixtures(t).NewProc(tt.process)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -102,7 +102,7 @@ func TestExecutable(t *testing.T) {
 		{process: 26231, want: "/usr/bin/vim"},
 		{process: 26232, want: ""},
 	} {
-		p, err := FS(procTestFixtures).NewProc(tt.process)
+		p, err := getProcFixtures(t).NewProc(tt.process)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -126,7 +126,7 @@ func TestCwd(t *testing.T) {
 		{process: 26232, want: "/does/not/exist", brokenLink: true},
 		{process: 26233, want: ""},
 	} {
-		p, err := FS(procTestFixtures).NewProc(tt.process)
+		p, err := getProcFixtures(t).NewProc(tt.process)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -154,7 +154,7 @@ func TestRoot(t *testing.T) {
 		{process: 26232, want: "/does/not/exist", brokenLink: true},
 		{process: 26233, want: ""},
 	} {
-		p, err := FS(procTestFixtures).NewProc(tt.process)
+		p, err := getProcFixtures(t).NewProc(tt.process)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -173,7 +173,7 @@ func TestRoot(t *testing.T) {
 }
 
 func TestFileDescriptors(t *testing.T) {
-	p1, err := FS(procTestFixtures).NewProc(26231)
+	p1, err := getProcFixtures(t).NewProc(26231)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -188,7 +188,7 @@ func TestFileDescriptors(t *testing.T) {
 }
 
 func TestFileDescriptorTargets(t *testing.T) {
-	p1, err := FS(procTestFixtures).NewProc(26231)
+	p1, err := getProcFixtures(t).NewProc(26231)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -210,7 +210,7 @@ func TestFileDescriptorTargets(t *testing.T) {
 }
 
 func TestFileDescriptorsLen(t *testing.T) {
-	p1, err := FS(procTestFixtures).NewProc(26231)
+	p1, err := getProcFixtures(t).NewProc(26231)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/stat_test.go b/stat_test.go
index 1e47a61..00c280b 100644
--- a/stat_test.go
+++ b/stat_test.go
@@ -16,7 +16,7 @@ package procfs
 import "testing"
 
 func TestStat(t *testing.T) {
-	s, err := FS(procTestFixtures).NewStat()
+	s, err := getProcFixtures(t).NewStat()
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/xfrm_test.go b/xfrm_test.go
index cedf862..4f06b96 100644
--- a/xfrm_test.go
+++ b/xfrm_test.go
@@ -18,7 +18,7 @@ import (
 )
 
 func TestXfrmStats(t *testing.T) {
-	xfrmStats, err := FS(procTestFixtures).NewXfrmStat()
+	xfrmStats, err := getProcFixtures(t).NewXfrmStat()
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/xfs/parse_test.go b/xfs/parse_test.go
index e2208f7..fcbe9ef 100644
--- a/xfs/parse_test.go
+++ b/xfs/parse_test.go
@@ -424,7 +424,14 @@ func TestParseStats(t *testing.T) {
 			stats, err = xfs.ParseStats(strings.NewReader(tt.s))
 		}
 		if tt.fs {
-			stats, err = xfs.ReadProcStat("../fixtures/proc")
+			xfs, err := xfs.NewFS("../fixtures/proc", "../fixtures/sys")
+			if err != nil {
+				t.Fatalf("failed to access xfs fs: %v", err)
+			}
+			stats, err = xfs.ProcStat()
+			if err != nil {
+				t.Fatalf("failed to gather xfs stats: %v", err)
+			}
 		}
 
 		if tt.invalid && err == nil {
diff --git a/xfs/xfs_test.go b/xfs/xfs_test.go
index d6f1fcb..03fe81e 100644
--- a/xfs/xfs_test.go
+++ b/xfs/xfs_test.go
@@ -21,7 +21,11 @@ import (
 )
 
 func TestReadProcStat(t *testing.T) {
-	stats, err := xfs.ReadProcStat("../fixtures/proc")
+	xfs, err := xfs.NewFS("../fixtures/proc", "../fixtures/sys")
+	if err != nil {
+		t.Fatalf("failed to access xfs fs: %v", err)
+	}
+	stats, err := xfs.ProcStat()
 	if err != nil {
 		t.Fatalf("failed to parse XFS stats: %v", err)
 	}
@@ -34,7 +38,11 @@ func TestReadProcStat(t *testing.T) {
 }
 
 func TestReadSysStats(t *testing.T) {
-	stats, err := xfs.ReadSysStats("../fixtures/sys")
+	xfs, err := xfs.NewFS("../fixtures/proc", "../fixtures/sys")
+	if err != nil {
+		t.Fatalf("failed to access xfs fs: %v", err)
+	}
+	stats, err := xfs.SysStats()
 	if err != nil {
 		t.Fatalf("failed to parse XFS stats: %v", err)
 	}