Skip to content

Commit

Permalink
Merge pull request #16 from 0x20F/testable-refactor
Browse files Browse the repository at this point in the history
Testable refactor
  • Loading branch information
0x20F authored Feb 19, 2022
2 parents 3f5ef4c + c0aede2 commit aa356bb
Show file tree
Hide file tree
Showing 29 changed files with 1,619 additions and 279 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ $ co2 store remove unique-store

<br/>

### 📦 `co2 service start`
### 📦 `co2 start`
Looks through all the registered stores (see [add](#%F0%9F%93%A6-co2-store-add) on how to register stores) and starts all of the provided services
if they're found.

Expand All @@ -195,7 +195,7 @@ If some of the provided services are already running but you'd like to stop them

<br/>

### 📦 `co2 service stop`
### 📦 `co2 stop`
Looks through the currently running **carbon** services and stops the provided ones.

Example:
Expand Down
107 changes: 66 additions & 41 deletions carbon/carbon.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"co2/types"
"io/ioutil"
"reflect"

"gopkg.in/yaml.v2"
)
Expand Down Expand Up @@ -60,57 +61,81 @@ func findCarbonFiles(root string, depth int) []string {
func Configurations(path string, depth int) types.CarbonConfig {
files := findCarbonFiles(path, depth)

var config types.CarbonConfig = make(types.CarbonConfig) // Abstraction for the fields we actually care about in a service definition
var values types.ServiceDefinition = make(types.ServiceDefinition) // Abstraction so we just get an accessible map of all the fields for later
var config types.CarbonConfig = make(types.CarbonConfig, len(files))

for _, file := range files {
content, err := ioutil.ReadFile(file)
if err != nil {
panic(err)
}

// Split at yaml document separator
documents := bytes.Split(content, []byte("---"))

for _, doc := range documents {
temp := types.CarbonConfig{}

// Unmarshal once into a structure with limited fields
err := yaml.Unmarshal(doc, &temp)
if err != nil {
panic(err)
}

// Unmarshal again into an arbitrary map with all the available fields
err = yaml.Unmarshal(doc, &values)
if err != nil {
panic(err)
}

// Inject the path into the config
for k, v := range temp {
v.Path = file
config[k] = v
}
documents := documents(content, file)

for k, v := range documents {
config[k] = v
}
}

// Map all the arbitrary maps into the CarbonYaml structures
// for later use.
count := 0
for key := range config {
actual := config[key]
arbitrary := values[key]

// Properly instantiate all the important fields
actual.Name = key
actual.FullContents = make(types.ServiceFields)
actual.FullContents = arbitrary

// Update with the finalized structure
config[key] = actual
count++
return config
}

// Separates the given file contents at the yaml document
// separator and parses all the found documents according to
// the carbon requirements.
//
// Each service will be parsed into a CarbonConfig, and then into
// an arbitrary map that contains all the fields of the defined configuration.
// This arbitrary map will then be mapped into a full CarbonConfig so that
// each service has access to all of the contents within their service definition
// if they ever need it.
func documents(contents []byte, file string) types.CarbonConfig {
documents := bytes.Split(contents, []byte("---"))

var final types.CarbonConfig = make(types.CarbonConfig)

for _, doc := range documents {
full := types.CarbonConfig{}
fake := types.ServiceDefinition{}

// Unmarshal once into a structure with limited fields
err := yaml.Unmarshal(doc, &full)
if err != nil {
panic(err)
}

// Unmarshal again into an arbitrary map with all the available fields
err = yaml.Unmarshal(doc, &fake)
if err != nil {
panic(err)
}

// Map the values from the fake map into the real map
k, v := move(fake, full, file)
final[k] = v
}

return config
return final
}

// Maps the required fields from the arbitrary map with no specific structure
// into the real CarbonConfig map with the correct structure.
//
// This makes sure that the final service representation knows the path
// it came from, the name of the service it represents, and has access
// to all of the contents within their service definition if they ever
// need it.
//
// This has to exist since we're not building a 1:1 mapping between a
// docker-compose service definition but we still want all the data.
func move(this types.ServiceDefinition, into types.CarbonConfig, file string) (string, types.CarbonService) {
// Get all the key from the map even though we know there's only 1
key := reflect.ValueOf(into).MapKeys()[0].String()
current := into[key]

current.Path = file
current.Name = key
current.FullContents = make(types.ServiceFields)
current.FullContents = this[key]

return key, current
}
88 changes: 88 additions & 0 deletions carbon/carbon_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package carbon

import (
"co2/types"
"testing"
)

// Note that this is indented with spaces.
// Tabs will break yaml so be careful.
var customDocument = `
test:
image: golang
depends_on:
- test-db
---
test-db:
image: lmao
ports:
- "8080:80"
`

func TestMovingDataBetweenDefinitions(t *testing.T) {
// Create a new service definition
def := types.ServiceDefinition{
"service": {
"unique-but-useless": "value",
},
}

// Create a new carbon config
config := types.CarbonConfig{
"service": {},
}

// Move the data from the service definition into the carbon config
k, v := move(def, config, "filename")

// Assert that the key is the same as the service name
if k != "service" {
t.Errorf("Expected key to be 'service', got '%s'", k)
}

// Make sure the path got set to filename
if v.Path != "filename" {
t.Errorf("Expected path to be 'filename', got '%s'", v.Path)
}

// Make sure the name got set to service
if v.Name != "service" {
t.Errorf("Expected name to be 'service', got '%s'", v.Name)
}

// Make sure the unique-but-useless is present in the full contents
if v.FullContents["unique-but-useless"] != "value" {
t.Errorf("Expected 'unique-but-useless' to be 'value', got '%s'", v.FullContents["unique-but-useless"])
}
}

func TestYamlParsingOfMultipleDocuments(t *testing.T) {
// Parse the yaml into a carbon config
config := documents([]byte(customDocument), "filename")

// Make sure the config has the correct number of services
if len(config) != 2 {
t.Errorf("Expected 2 services, got %d", len(config))
}

// Make sure the path is filename for each of the services
if config["test"].Path != "filename" {
t.Errorf("Expected path to be 'filename', got '%s'", config["test"].Path)
}

if config["test-db"].Path != "filename" {
t.Errorf("Expected path to be 'filename', got '%s'", config["test-db"].Path)
}

// Make sure the config has the correct number of fields
if len(config["test"].FullContents) != 2 {
t.Errorf("Expected 1 field, got %d", len(config["test"].FullContents))
}

// Make sure the config has the correct number of fields
if len(config["test-db"].FullContents) != 2 {
t.Errorf("Expected 2 fields, got %d", len(config["test-db"].FullContents))
}
}
51 changes: 51 additions & 0 deletions cmd/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cmd

import (
"co2/carbon"
"co2/database"
"co2/types"
)

var fs FsWrapper = &impl{}

type FsWrapper interface {
Services() types.CarbonConfig
}

type impl struct{}

// Looks through all the registered stores and returns all
// the carbon services that are defined within those stores.
//
// This will never to too deep into the stores when looking
// for services since we want it to be fast. Usually a depth of 2
// is enough.
//
// Each of the returned configurations will have the store
// they belong to injected as well so they can retrieve
// the required data if ever needed.
func (i *impl) Services() types.CarbonConfig {
stores := database.Stores()
configs := types.CarbonConfig{}

for _, store := range stores {
files := carbon.Configurations(store.Path, 2)

for k, v := range files {
v.Store = &store
configs[k] = v
}
}

return configs
}

// Replaces the default Fs instance with a custom
// implementation.
//
// Note that this exists for the sole purpose of unit testing.
// It makes it easy to replace how we access the carbon methods
// during tests.
func WrapFs(custom FsWrapper) {
fs = custom
}
37 changes: 3 additions & 34 deletions cmd/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,8 @@ import (
"co2/docker"
"co2/types"
"testing"

"github.com/4khara/replica"
dockerTypes "github.com/docker/docker/api/types"
)

type MockWrapper struct{}

func (w *MockWrapper) RunningContainers() []dockerTypes.Container {
replica.MockFn()

return []dockerTypes.Container{
{
ID: "1",
Image: "image1",
Names: []string{"/docker-container1"},
},
{
ID: "2",
Image: "image2",
Names: []string{"/docker-container2"},
},
{
ID: "3",
Image: "image3",
Names: []string{"/docker-container3"},
},
}
}

func before() {
docker.CustomWrapper(&MockWrapper{})
}

func TestShouldRunLogsCommandReturnsFalseWithNoCommands(t *testing.T) {
commands := []types.Command{}

Expand Down Expand Up @@ -93,7 +62,7 @@ func TestCommandLabelMatchesContainerName(t *testing.T) {
}

func TestContainersFilterFindsByUid(t *testing.T) {
before()
beforeCmdTest()

// Get the containers that docker will return so we can get the hashes
dockerContainers := docker.RunningContainers()
Expand All @@ -108,7 +77,7 @@ func TestContainersFilterFindsByUid(t *testing.T) {
}

func TestContainersFilterFindsByCarbonServiceName(t *testing.T) {
before()
beforeCmdTest()

containers := []types.Container{
{
Expand All @@ -134,7 +103,7 @@ func TestContainersFilterFindsByCarbonServiceName(t *testing.T) {
}

func TestContainersMatchesBothUidAndServiceName(t *testing.T) {
before()
beforeCmdTest()

containers := []types.Container{
{
Expand Down
Loading

0 comments on commit aa356bb

Please sign in to comment.