diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..4b86d7197f9a6f1a988c503defc8451392cd5e55 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Getting Help on IPFS + url: https://ipfs.io/help + about: All information about how and where to get help on IPFS. + - name: IPFS Official Forum + url: https://discuss.ipfs.io + about: Please post general questions, support requests, and discussions here. diff --git a/.github/ISSUE_TEMPLATE/open_an_issue.md b/.github/ISSUE_TEMPLATE/open_an_issue.md new file mode 100644 index 0000000000000000000000000000000000000000..4fcbd00aca0d513c2729d8df4afb5a01fdbe7d02 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/open_an_issue.md @@ -0,0 +1,19 @@ +--- +name: Open an issue +about: Only for actionable issues relevant to this repository. +title: '' +labels: need/triage +assignees: '' + +--- + diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed26646a0f7cdda6cf10ede2b0b98cac89cf67b0 --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,68 @@ +# Configuration for welcome - https://github.com/behaviorbot/welcome + +# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome +# Comment to be posted to on first time issues +newIssueWelcomeComment: > + Thank you for submitting your first issue to this repository! A maintainer + will be here shortly to triage and review. + + In the meantime, please double-check that you have provided all the + necessary information to make this process easy! Any information that can + help save additional round trips is useful! We currently aim to give + initial feedback within **two business days**. If this does not happen, feel + free to leave a comment. + + Please keep an eye on how this issue will be labeled, as labels give an + overview of priorities, assignments and additional actions requested by the + maintainers: + + - "Priority" labels will show how urgent this is for the team. + - "Status" labels will show if this is ready to be worked on, blocked, or in progress. + - "Need" labels will indicate if additional input or analysis is required. + + Finally, remember to use https://discuss.ipfs.io if you just need general + support. + +# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome +# Comment to be posted to on PRs from first time contributors in your repository +newPRWelcomeComment: > + Thank you for submitting this PR! + + A maintainer will be here shortly to review it. + + We are super grateful, but we are also overloaded! Help us by making sure + that: + + * The context for this PR is clear, with relevant discussion, decisions + and stakeholders linked/mentioned. + + * Your contribution itself is clear (code comments, self-review for the + rest) and in its best form. Follow the [code contribution + guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md#code-contribution-guidelines) + if they apply. + + Getting other community members to do a review would be great help too on + complex PRs (you can ask in the chats/forums). If you are unsure about + something, just leave us a comment. + + Next steps: + + * A maintainer will triage and assign priority to this PR, commenting on + any missing things and potentially assigning a reviewer for high + priority items. + + * The PR gets reviews, discussed and approvals as needed. + + * The PR is merged by maintainers when it has been approved and comments addressed. + + We currently aim to provide initial feedback/triaging within **two business + days**. Please keep an eye on any labelling actions, as these will indicate + priorities and status of your contribution. + + We are very grateful for your contribution! + + +# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge +# Comment to be posted to on pull requests merged by a first time user +# Currently disabled +#firstPRMergeComment: "" diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 0000000000000000000000000000000000000000..32bcc2e6e571121eee3c966f2f31bb86c5430e93 --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,51 @@ +# File managed by web3-bot. DO NOT EDIT. +# See https://github.com/protocol/.github/ for details. + +# Automatically merge pull requests opened by web3-bot, as soon as (and only if) all tests pass. +# This reduces the friction associated with updating with our workflows. + +on: [ pull_request ] +name: Automerge + +jobs: + automerge-check: + if: github.event.pull_request.user.login == 'web3-bot' + runs-on: ubuntu-latest + outputs: + status: ${{ steps.should-automerge.outputs.status }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Check if we should automerge + id: should-automerge + run: | + for commit in $(git rev-list --first-parent origin/${{ github.event.pull_request.base.ref }}..${{ github.event.pull_request.head.sha }}); do + committer=$(git show --format=$'%ce' -s $commit) + echo "Committer: $committer" + if [[ "$committer" != "web3-bot@users.noreply.github.com" ]]; then + echo "Commit $commit wasn't committed by web3-bot, but by $committer." + echo "::set-output name=status::false" + exit + fi + done + echo "::set-output name=status::true" + automerge: + needs: automerge-check + runs-on: ubuntu-latest + if: ${{ needs.automerge-check.outputs.status == 'true' }} + steps: + - name: Wait on tests + uses: lewagon/wait-on-check-action@bafe56a6863672c681c3cf671f5e10b20abf2eaa # v0.2 + with: + ref: ${{ github.event.pull_request.head.sha }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + wait-interval: 10 + running-workflow-name: 'automerge' # the name of this job + - name: Merge PR + uses: pascalgn/automerge-action@741c311a47881be9625932b0a0de1b0937aab1ae # v0.13.1 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + MERGE_LABELS: "" + MERGE_METHOD: "squash" + MERGE_DELETE_BRANCH: true diff --git a/.github/workflows/go-check.yml b/.github/workflows/go-check.yml new file mode 100644 index 0000000000000000000000000000000000000000..00ce947c82a56a488b0fc018e4a7dbf2fad2e95e --- /dev/null +++ b/.github/workflows/go-check.yml @@ -0,0 +1,50 @@ +# File managed by web3-bot. DO NOT EDIT. +# See https://github.com/protocol/.github/ for details. + +on: [push, pull_request] +name: Go Checks + +jobs: + unit: + runs-on: ubuntu-latest + name: All + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + - uses: actions/setup-go@v2 + with: + go-version: "1.16.x" + - name: Install staticcheck + run: go install honnef.co/go/tools/cmd/staticcheck@434f5f3816b358fe468fa83dcba62d794e7fe04b # 2021.1 (v0.2.0) + - name: Check that go.mod is tidy + uses: protocol/multiple-go-modules@v1.0 + with: + run: | + go mod tidy + if [[ -n $(git ls-files --other --exclude-standard --directory -- go.sum) ]]; then + echo "go.sum was added by go mod tidy" + exit 1 + fi + git diff --exit-code -- go.sum go.mod + - name: gofmt + if: ${{ success() || failure() }} # run this step even if the previous one failed + run: | + out=$(gofmt -s -l .) + if [[ -n "$out" ]]; then + echo $out | awk '{print "::error file=" $0 ",line=0,col=0::File is not gofmt-ed."}' + exit 1 + fi + - name: go vet + if: ${{ success() || failure() }} # run this step even if the previous one failed + uses: protocol/multiple-go-modules@v1.0 + with: + run: go vet ./... + - name: staticcheck + if: ${{ success() || failure() }} # run this step even if the previous one failed + uses: protocol/multiple-go-modules@v1.0 + with: + run: | + set -o pipefail + staticcheck ./... | sed -e 's@\(.*\)\.go@./\1.go@g' + diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml new file mode 100644 index 0000000000000000000000000000000000000000..4c7138b01d0c904d152e351b15e45ff2a413174d --- /dev/null +++ b/.github/workflows/go-test.yml @@ -0,0 +1,47 @@ +# File managed by web3-bot. DO NOT EDIT. +# See https://github.com/protocol/.github/ for details. + +on: [push, pull_request] +name: Go Test + +jobs: + unit: + strategy: + fail-fast: false + matrix: + os: [ "ubuntu", "windows", "macos" ] + go: [ "1.15.x", "1.16.x" ] + runs-on: ${{ matrix.os }}-latest + name: ${{ matrix.os}} (go ${{ matrix.go }}) + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + - name: Go information + run: | + go version + go env + - name: Run tests + uses: protocol/multiple-go-modules@v1.0 + with: + run: go test -v -coverprofile coverage.txt ./... + - name: Run tests (32 bit) + if: ${{ matrix.os != 'macos' }} # can't run 32 bit tests on OSX. + uses: protocol/multiple-go-modules@v1.0 + env: + GOARCH: 386 + with: + run: go test -v ./... + - name: Run tests with race detector + if: ${{ matrix.os == 'ubuntu' }} # speed things up. Windows and OSX VMs are slow + uses: protocol/multiple-go-modules@v1.0 + with: + run: go test -v -race ./... + - name: Upload coverage to Codecov + uses: codecov/codecov-action@a1ed4b322b4b38cb846afb5a0ebfa17086917d27 # v1.5.0 + with: + file: coverage.txt + env_vars: OS=${{ matrix.os }}, GO=${{ matrix.go }} diff --git a/.gx/lastpubver b/.gx/lastpubver new file mode 100644 index 0000000000000000000000000000000000000000..287f0384c0a50700b2a029a59fba436e6f20d558 --- /dev/null +++ b/.gx/lastpubver @@ -0,0 +1 @@ +0.0.1: QmZUbTDJ39JpvtFCSubiWeUTQRvMA1tVE5RZCJrY4oeAsC diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..e4224df5b7a109818bddc7862137f495f2c2b82e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 IPFS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 29d9f91ef1de324850cb0f0f412d2af08fbfb22f..df2157fc2560eced888fd5c6ada8ddeadf456ec8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,42 @@ -# go-dms3-pq +# go-ipfs-pq -dms3 priority queue \ No newline at end of file +[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) +[![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://ipfs.io/) +[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) +[![GoDoc](https://godoc.org/github.com/ipfs/go-ipfs-pq?status.svg)](https://godoc.org/github.com/ipfs/go-ipfs-pq) +[![Build Status](https://travis-ci.org/ipfs/go-ipfs-pq.svg?branch=master)](https://travis-ci.org/ipfs/go-ipfs-pq) + +> go-ipfs-pq implements a priority queue. + +## Table of Contents + +- [Install](#install) +- [Usage](#usage) +- [Contribute](#contribute) +- [License](#license) + +## Install + +`go-ipfs-pq` works like a regular Go module: + +``` +> go get github.com/ipfs/go-ipfs-pq +``` + +## Usage + +``` +import "github.com/ipfs/go-ipfs-pq" +``` + +Check the [GoDoc documentation](https://godoc.org/github.com/ipfs/go-ipfs-pq) + +## Contribute + +PRs accepted. + +Small note: If editing the README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. + +## License + +MIT © Protocol Labs, Inc. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..1b29a50d7ec15271c8fa44fa2e130fce8b631a32 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitlab.dms3.io/dms3/public/go-ipfs-pq + +go 1.15 diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..329e1c130c4d727d07fa525a507ca4f6544fafeb --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "author": "hsanjuan", + "bugs": { + "url": "https://github.com/ipfs/go-ipfs-pq" + }, + "gx": { + "dvcsimport": "github.com/ipfs/go-ipfs-pq" + }, + "gxVersion": "0.12.1", + "language": "go", + "license": "MIT", + "name": "go-ipfs-pq", + "releaseCmd": "git commit -a -m \"gx publish $VERSION\"", + "version": "0.0.1" +} + diff --git a/pq.go b/pq.go new file mode 100644 index 0000000000000000000000000000000000000000..389e15b8fe2f775fa3c41e50656853a53c71adbe --- /dev/null +++ b/pq.go @@ -0,0 +1,118 @@ +// Package pq implements a priority queue. +package pq + +import "container/heap" + +// PQ is a basic priority queue. +type PQ interface { + // Push adds the ele + Push(Elem) + // Pop removes and returns the highest priority Elem in PQ. + Pop() Elem + // Peek returns the highest priority Elem in PQ (without removing it). + Peek() Elem + // Remove removes the item at the given index from the PQ. + Remove(index int) Elem + // Len returns the number of elements in the PQ. + Len() int + // Update `fixes` the PQ. + Update(index int) +} + +// Elem describes elements that can be added to the PQ. Clients must implement +// this interface. +type Elem interface { + // SetIndex stores the int index. + SetIndex(int) + // Index returns the last given by SetIndex(int). + Index() int +} + +// ElemComparator returns true if pri(a) > pri(b) +type ElemComparator func(a, b Elem) bool + +// New creates a PQ with a client-supplied comparator. +func New(cmp ElemComparator) PQ { + q := &wrapper{heapinterface{ + elems: make([]Elem, 0), + cmp: cmp, + }} + heap.Init(&q.heapinterface) + return q +} + +// wrapper exists because we cannot re-define Push. We want to expose +// Push(Elem) but heap.Interface requires Push(interface{}) +type wrapper struct { + heapinterface +} + +var _ PQ = &wrapper{} + +func (w *wrapper) Push(e Elem) { + heap.Push(&w.heapinterface, e) +} + +func (w *wrapper) Pop() Elem { + return heap.Pop(&w.heapinterface).(Elem) +} + +func (w *wrapper) Peek() Elem { + if len(w.heapinterface.elems) == 0 { + return nil + } + return w.heapinterface.elems[0] +} + +func (w *wrapper) Remove(index int) Elem { + return heap.Remove(&w.heapinterface, index).(Elem) +} + +func (w *wrapper) Update(index int) { + heap.Fix(&w.heapinterface, index) +} + +// heapinterface handles dirty low-level details of managing the priority queue. +type heapinterface struct { + elems []Elem + cmp ElemComparator +} + +var _ heap.Interface = &heapinterface{} + +// public interface + +func (q *heapinterface) Len() int { + return len(q.elems) +} + +// Less delegates the decision to the comparator +func (q *heapinterface) Less(i, j int) bool { + return q.cmp(q.elems[i], q.elems[j]) +} + +// Swap swaps the elements with indexes i and j. +func (q *heapinterface) Swap(i, j int) { + q.elems[i], q.elems[j] = q.elems[j], q.elems[i] + q.elems[i].SetIndex(i) + q.elems[j].SetIndex(j) +} + +// Note that Push and Pop in this interface are for package heap's +// implementation to call. To add and remove things from the heap, wrap with +// the pq struct to call heap.Push and heap.Pop. + +func (q *heapinterface) Push(x interface{}) { // where to put the elem? + t := x.(Elem) + t.SetIndex(len(q.elems)) + q.elems = append(q.elems, t) +} + +func (q *heapinterface) Pop() interface{} { + old := q.elems + n := len(old) + elem := old[n-1] // remove the last + elem.SetIndex(-1) // for safety // FIXME why? + q.elems = old[0 : n-1] // shrink + return elem +} diff --git a/pq_test.go b/pq_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5e53628804b61b53b181e1987088a2c6925ff4fa --- /dev/null +++ b/pq_test.go @@ -0,0 +1,119 @@ +package pq + +import ( + "sort" + "testing" +) + +type TestElem struct { + Key string + Priority int + index int +} + +func (e *TestElem) Index() int { + return e.index +} + +func (e *TestElem) SetIndex(i int) { + e.index = i +} + +var PriorityComparator = func(i, j Elem) bool { + return i.(*TestElem).Priority > j.(*TestElem).Priority +} + +func TestQueuesReturnTypeIsSameAsParameterToPush(t *testing.T) { + q := New(PriorityComparator) + expectedKey := "foo" + elem := &TestElem{Key: expectedKey} + q.Push(elem) + switch v := q.Pop().(type) { + case *TestElem: + if v.Key != expectedKey { + t.Fatal("the key doesn't match the pushed value") + } + default: + t.Fatal("the queue is not casting values appropriately") + } +} + +func TestCorrectnessOfPop(t *testing.T) { + q := New(PriorityComparator) + tasks := []TestElem{ + {Key: "a", Priority: 9}, + {Key: "b", Priority: 4}, + {Key: "c", Priority: 3}, + {Key: "d", Priority: 0}, + {Key: "e", Priority: 6}, + } + for _, e := range tasks { + q.Push(&e) + } + var priorities []int + var peekPriorities []int + for q.Len() > 0 { + i := q.Peek().(*TestElem).Priority + peekPriorities = append(peekPriorities, i) + j := q.Pop().(*TestElem).Priority + t.Logf("popped %v", j) + priorities = append(priorities, j) + } + if !sort.IntsAreSorted(peekPriorities) { + t.Fatal("the values were not returned in sorted order") + } + if !sort.IntsAreSorted(priorities) { + t.Fatal("the popped values were not returned in sorted order") + } +} + +func TestRemove(t *testing.T) { + q := New(PriorityComparator) + tasks := []TestElem{ + {Key: "a", Priority: 9}, + {Key: "b", Priority: 4}, + {Key: "c", Priority: 3}, + } + for i := range tasks { + q.Push(&tasks[i]) + } + removed := q.Remove(1).(*TestElem) + if q.Len() != 2 { + t.Fatal("expected item to have been removed") + } + if q.Pop().(*TestElem).Key == removed.Key { + t.Fatal("Remove() returned wrong element") + } + if q.Pop().(*TestElem).Key == removed.Key { + t.Fatal("Remove() returned wrong element") + } +} + +func TestUpdate(t *testing.T) { + t.Log(` + Add 3 elements. + Update the highest priority element to have the lowest priority and fix the queue. + It should come out last.`) + q := New(PriorityComparator) + lowest := &TestElem{Key: "originallyLowest", Priority: 1} + middle := &TestElem{Key: "originallyMiddle", Priority: 2} + highest := &TestElem{Key: "toBeUpdated", Priority: 3} + q.Push(middle) + q.Push(highest) + q.Push(lowest) + if q.Peek().(*TestElem).Key != highest.Key { + t.Fatal("head element doesn't have the highest priority") + } + if q.Pop().(*TestElem).Key != highest.Key { + t.Fatal("popped element doesn't have the highest priority") + } + q.Push(highest) // re-add the popped element + highest.Priority = 0 // update the PQ + q.Update(highest.Index()) // fix the PQ + if q.Peek().(*TestElem).Key != middle.Key { + t.Fatal("middle element should now have the highest priority") + } + if q.Pop().(*TestElem).Key != middle.Key { + t.Fatal("middle element should now have the highest priority") + } +}