An Early 2019 Update

Happy New Year!

(over a month late)

This post is a status update covering the things I have been and will be up to.

Closing 2019

Freelancing: Cucumber

Matt Wynne and I got together to plan out some work for me to do for their video training course called Cucumber School. They’ve had these videos out for a while and they generate revenue, but they were missing subtitles for folks who couldn’t hear the audio. I’ll be writing an article on the Cucumber blog about this, but for now, here’s a screenshot of what my workflow looked like in Youtube for this:

Learning: stardew-crops

For the end of the year, I took a long vacation to get some time out of the office. My partner and I didn’t go anywhere, but having an extended time out of work was quite nice. I spent a good amount of that time writing code which was great! The code that came out of that was a CLI tool I’ve called stardew-crops.

The command: stardew-crops search -f pretty --season summer --growthgt 5 --growthlt 7 --continuous returns the following:

╔════════════════════════════════════════════════╗
║ Results:                                       ║
║ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═║
║ 1.                                             ║
║ Name: Hot Pepper                               ║
║ Seeds: Pepper Seeds                            ║
║ Growth Time: 5 days                            ║
║ Season: Summer                                 ║
║ Continual: Yes                                 ║
║ Regrowth: 3 days                               ║
║ Seed Price:                                    ║
║   * General Store: 40g                         ║
║   * JojaMart: 50g                              ║
║ Bundles:                                       ║
║   * Summer Crops                               ║
║ Recipes:                                       ║
║   * Pepper Poppers                             ║
║   * Spicy Eel                                  ║
║ Quests:                                        ║
║   * Knee Therapy                               ║
║   * Summer Help Wanted                         ║
║ Notes:                                         ║
║   * Has a small chance for multiple fruit from ║
║     each harvest                               ║
╚════════════════════════════════════════════════╝

Stardew-crops uses Cobra for its CLI framework and in addition to getting both solid unit and behavioral test coverage, I tried to write each component in an isolated way. For instance, command files are just a bridge for Cobra into the actual processor files that perform the behavior of that command.

...
// SearchCmd returns data based on the input crop
var SearchCmd = &cobra.Command{
    Use:   "search",
    Short: "Search for crops via a number of methods",
    Long:  "Search returns a list of crops based on the provided filters and search criteria",
    Run:   Search,
}

// Search is the main orchestrator for the cli's
// search functionality
func Search(cmd *cobra.Command, args []string) {
    userFlags := utils.UserSetFlags(cmd)

    // return help information if no flags are provided
    if len(userFlags) == 0 {
        cmd.Help()
        return
    }

    b := bytes.NewBuffer([]byte{})
    out, err := processors.Search(userFlags)
    if err != nil {
        b = formatter.Format(err.Error(), "error")
    } else {
        b = formatter.Format(out, userFlags["format"])
    }

    print(b)
}
...

The search function in the processor’s package determines what crops in the data it has are valid to return to the user based on their flags. The first round of writing the search processor, I was iterating through each flag and checking each crop’s validity. Yuck! In some refactoring, I changed things so that I only went through the list of crops once, checking each flag then. If any flag would disqualify that particular crop, we’d move on, meaning that only the crops that made it all the way through were returned in the data set.

// Search ...
func Search(flags map[string]string) (data.CropData, error) {
    var out data.CropData

    c := cropData.Crops
    for i := 0; i < len(cropData.Crops); i++ {
        specified, ok, err := byBundle(c[i], flags)
        if err != nil {
            return data.CropData{}, err
        } else if specified && !ok {
            continue
        }

        specified, ok, err = byGrowth("g", c[i].Info.GrowthTime, flags)
        if err != nil {
            return data.CropData{}, err
        } else if specified && !ok {
            continue
        }
...
            specified, ok, err = byContinuous(c[i].Info.Continual, flags)
        if err != nil {
            return data.CropData{}, err
        } else if specified && !ok {
            continue
        }

        out.Crops = append(out.Crops, c[i])
    }

    if len(out.Crops) == 0 {
        return data.CropData{}, fmt.Errorf("No matching crops found")
    }

    return out, nil
}

The CLI has three different output options available in the formatter. Using the same search query above:

  • -f raw - returns the direct, unformatted json data {"crops":[{"name":"hot pepper","info":{"description":"Fiery hot with a hint of sweetness.","seed":"pepper seeds","growth_time":5,"season":["summer"],"continual":true,"regrowth":3},"seed_prices":{"general_store":40,"jojamart":50},"bundles":["Summer Crops"],"recipes":["Pepper Poppers","Spicy Eel"],"quests":["Knee Therapy","Summer Help Wanted"],"notes":["Has a small chance for multiple fruit from each harvest"]}]}
  • -f json - returns formatted json json { "crops": [ { "name": "hot pepper", "info": { "description": "Fiery hot with a hint of sweetness.", "seed": "pepper seeds", "growth_time": 5, "season": ["summer"], "continual": true, "regrowth": 3 }, "seed_prices": { "general_store": 40, "jojamart": 50 }, "bundles": ["Summer Crops"], "recipes": ["Pepper Poppers", "Spicy Eel"], "quests": ["Knee Therapy", "Summer Help Wanted"], "notes": ["Has a small chance for multiple fruit from each harvest"] } ] }
  • -f pretty - a pretty formatted response. ╔════════════════════════════════════════════════╗ ║ Results: ║ ║ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═║ ║ 1. ║ ║ Name: Hot Pepper ║ ║ Seeds: Pepper Seeds ║ ║ Growth Time: 5 days ║ ║ Season: Summer ║ ║ Continual: Yes ║ ║ Regrowth: 3 days ║ ║ Seed Price: ║ ║ * General Store: 40g ║ ║ * JojaMart: 50g ║ ║ Bundles: ║ ║ * Summer Crops ║ ║ Recipes: ║ ║ * Pepper Poppers ║ ║ * Spicy Eel ║ ║ Quests: ║ ║ * Knee Therapy ║ ║ * Summer Help Wanted ║ ║ Notes: ║ ║ * Has a small chance for multiple fruit from ║ ║ each harvest ║ ╚════════════════════════════════════════════════╝

The pretty formatter uses the following template to generate its response:

╔════════════════════════════════════════════════╗
 Results:                                       
                        ═║
{{ range $i, $c := .Crops -}}
{{ if gt $i 0 -}}
 {{ pad 46 ""}} 
{{end -}}
 {{add $i 1 | printf "%d." | pad 46 }} 
 Name: {{pad 40 $c.Name | title}} 
 Seeds: {{pad 39 $c.Info.Seed | title}} 
 Growth Time: {{printf "%d days" $c.Info.GrowthTime | pad 33}} 
 Season: {{season $c.Info.Season | pad 38 }} 
{{ if $c.Info.Continual -}}
 Continual: Yes                                 
 Regrowth: {{printf "%d days" $c.Info.Regrowth | pad 36}} 
{{end -}}
 Seed Price:                                    
{{ $sp := $c.SeedPrices -}}
{{ if gt $sp.GeneralStore 0 -}}
   * General Store: {{printf "%dg" $sp.GeneralStore | pad 27}} 
{{end -}}
{{ if gt $sp.JojaMart 0 -}}
   * JojaMart: {{printf "%dg" $sp.JojaMart | pad 32}} 
{{end -}}
{{ $tc := derefTCP $sp -}}
{{ if gt $tc.Min 0 -}}
   * Traveling Cart:                            
      * Min: {{printf "%dg" $tc.Min | pad 34}} 
      * Max: {{printf "%dg" $tc.Max | pad 34}} 
{{end -}}
{{ if gt $sp.Oasis 0 -}}
   * Oasis: {{printf "%dg" $sp.Oasis | pad 35}} 
{{end -}}
{{ if gt $sp.EggFestival 0 -}}
   * Egg Festival: {{printf "%dg" $sp.EggFestival | pad 28}} 
{{end -}}
{{ if ge (len $c.Bundles) 1 -}}
{{- $ld := listData "Bundles" $c.Bundles -}}
{{- template "list" $ld -}}
{{ end -}}
{{ if ge (len $c.Recipes) 1 -}}
{{- $ld := listData "Recipes" $c.Recipes -}}
{{- template "list" $ld -}}
{{ end -}}
{{ if ge (len $c.Quests) 1 -}}
{{- $ld := listData "Quests" $c.Quests -}}
{{- template "list" $ld -}}
{{ end -}}
{{ if ge (len $c.Notes) 1 -}}
{{- $ld := listData "Notes" $c.Notes -}}
{{- template "list" $ld -}}
{{ end -}}
{{ end -}}
╚════════════════════════════════════════════════╝

{{- define "list" -}}
 {{ printf "%s:" .Name | pad 46 }} 
{{ range $q := .Items -}}
{{ range $l := lineSplit $q 42 -}}
 {{- printf "%s" $l | safe -}} 
{{ end -}}
{{ end -}}
{{ end -}}

One of the features that I’m most proud of for this project is the data wrapping for lines that are longer than the pretty formatter’s fixed-width window. I wanted to keep the data within the boundaries of the window but I also wanted to make sure that words weren’t chopped off mid-word, so the closest previous space would be found and the line would split there. The biggest challenge to solving for this is managing the splitting itself.

With a variable line length and not knowing where a space is beforehand, accounting for splitting occurring in different places is a requirement for correct output. In addition to not wanting to split inside words, we also don’t want to count leading or trailing spaces and we don’t want to output those.

Here’s my test to get various use-cases handled:

func TestLineBreaks(t *testing.T) {
    testCases := []struct {
        name       string
        input      string
        lineLength int
        expected   []string
    }{
        {
            name:       "Line length is over input length",
            input:      "xxxxxx xxxxxx",
            lineLength: 14,
            expected:   []string{"xxxxxx xxxxxx"},
        },
        {
            name:       "Line break at natural break following word",
            input:      "xxxxxx xxxxxx",
            lineLength: 6,
            expected:   []string{"xxxxxx", "xxxxxx"},
        },
        {
            name:       "Line break at natural break following space",
            input:      "xxxxxx xxxxxx",
            lineLength: 7,
            expected:   []string{"xxxxxx", "xxxxxx"},
        },
        {
            name:       "Break in the middle of a two word input",
            input:      "abcdef ghijkl",
            lineLength: 10,
            expected:   []string{"abcdef", "ghijkl"},
        },
        {
            name:       "Break in the middle of a short word",
            input:      "game. Starfruit produces Artisan Goods that have some of the highest sell values in",
            lineLength: 42,
            expected:   []string{"game. Starfruit produces Artisan Goods", "that have some of the highest sell values", "in"},
        },
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            actual := lineBreaks(tc.input, tc.lineLength)

            assert.Equal(t, tc.expected, actual)
        })
    }
}

The formatter items related this return a slice of appropriately lengthed lines for easy iteration in the template:

{{- define "list" -}}
 {{ printf "%s:" .Name | pad 46 }} 
{{ range $q := .Items -}}
{{ range $l := lineSplit $q 42 -}}
 {{- printf "%s" $l | safe -}} 
{{ end -}}
{{ end -}}
{{ end -}}

The New Year

Game Design

I started a new project in 2019: a game!

I’ve been playing a game on my phone called Tents and Trees that’s uses logic to solve a game board with elements of Picross puzzles and Minesweeper. It’s pretty great! Given there are very set rules and a handful of steps to help you determine the solution to each puzzle, I had thought about being able to write a solver. Somewhere along the lines, I ended up deciding to just write my own version of it. So on January 6, I started work on gopherhole. Given I’m writing it in Go and I didn’t want to completely copy it, I decided to theme it off of gopher’s and their habitats.

This game will be terminal based which will utilize the knowledge I’ve picked up learning CLI’s and eventually use one of Go’s terminal GUI packages like termui

The bulk of my work has been on trying to get a two-dimensional array set up for managing the board positions and populating it based on the rules of the game.

  • Each hole on the grid must have a gopher next to it
  • Gophers must be in one of the vertically or horizontally adjacent spaces to a hole
  • Gophers cannot be placed within the surrounding 8 spaces of another gopher

Here’s an example of a generated board:

{
    []string{" ", " ", "o", " ", " "},
    []string{"g", "o", "g", " ", " "},
    []string{" ", " ", " ", "o", "g"},
    []string{"g", " ", " ", " ", " "},
    []string{"o", " ", "o", "g", " "},
}

Blogging elsewhere

Aside from writing here, I’ve also got posts on the Cucumber and CircleCI blogs.

More Freelancing

I’m working with the Cucumber team some more on a project to migrate their blog from a Frankenstein Jekyll blog hosted on Heroku to one hosted with Ghost. I initially spent some time trying to automate the transformation of YAML+Markdown into Ghost’s desired JSON formatting including writing a lexer and parser based on the awesome book “Writing An Interpreter In Go” by Thorsten Ball but ran into some issues with getting a representative demo JSON file importing properly. Turns out Ghost’s documentation requires some nodes but doesn’t clearly denote that. (Side note: Ghost’s support team is AWESOME and super responsive. A+++!) After crafting some JSON to migrate users, I ended up migrating posts by hand as it didn’t take too long. Finishing the project and expanding it to support TOML, JSON, and whatever else may be a neat exercise.

My main tasks left for the blog migration are to migrate Discus comments and get routing updated. The comment migration just takes a CSV mapping the existing URL to the new one. I’ve just gotta give that to Discus to process and that should be good. The routing should be fairly straightforward, too, I’ve just gotta tweak the website’s NGINX config. The switch itself won’t be too hard, but I also want to make sure that the old links continue to work and I’m not currently aware of how to do that. I’m sure I can figure it out though! After this, we want to get the rest of the website off the Heroku box which includes a few other sites.

Going through this project I’ve run into some turbulence over my lack of web development skills trying to get the blog to look like Cucumber’s current theme. Perhaps once I get through this project and finish up the game I’ll spend some time getting proficient with web development. I’d definitely like to add more skills to my portfolio and there’s definitely no waste in learning that.

Meetups

I’m still running monthly meetups with my chapter of the Ministry of Testing. Our next meetup is on this upcoming Monday where we’ll be watching and discussing a conference talk by Justin Searls on mocking in tests. I think it’s going to be a great time. The folks who attend are eager to learn and discuss which is awesome. I just wish I could get more folks to come out and also volunteer to speak more.

Transition

In other news, I’ve officially made the jump from Software Engineer in Test to Software Engineer! 🎉 I’m looking forward to continuing to learn and expand my knowledge/experience and excited to see what opportunities open for me in the future!

Thanks so much for reading, feel free to reach out with comments or reach out another way.

– Jayson