Skip to content
This repository was archived by the owner on Oct 27, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/atcoder.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Test (atcoder)

on:
push:
branches: ['**']
tags-ignore: ['*']
paths: ['atcoder/*']

workflow_dispatch:

jobs:
test:
name: Test
runs-on: ubuntu-latest

steps:
- name: Set up Go 1.15
uses: actions/setup-go@v2
with:
go-version: 1.15

- name: Checkout Project
uses: actions/checkout@v2

- name: Cache Dependencies
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-go-

- name: Run Tests
run: go test -v -coverprofile=c.out ./atcoder
env:
BROWSER_HEADLESS:
BROWSER_BINARY: google-chrome

ATCODER_USERNAME: cptools
ATCODER_PASSWORD: ${{ secrets.ATCODER_PASSWORD }}

- name: Upload Coverage
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: c.out

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@

# Dependency directories (remove the comment below to include it)
vendor/

.env
88 changes: 88 additions & 0 deletions atcoder/atcoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package atcoder

import (
"fmt"
"regexp"
"strings"

"github.com/cp-tools/cpt-lib/util"

"github.com/go-rod/rod"
)

type (
// Args holds specifier details parsed by
// Parse() function. All methods use this
// at the core.
Args struct {
Contest string
Problem string
}

page struct {
*rod.Page
}
)

// Errors returned by library.
var (
ErrInvalidSpecifier = fmt.Errorf("invalid specifier data")
)

var (
hostURL = "https://atcoder.jp"

// Browser is the headless browser to use.
Browser *rod.Browser
)

// Start initiates the automated browser to use.
func Start(headless bool, userDataDir, bin string) error {
bs, err := util.NewBrowser(headless, userDataDir, bin)
Browser = bs

return err
}

// Parse passed in specifier string to new Args struct.
// Validates parsed args and returns error if any.
func Parse(str string) (Args, error) {
var (
rxCont = `(?P<cont>[A-Za-z0-9-]+)`
rxProb = `(?P<prob>[A-Za-z0-9_]+)`

valRx = []string{
`atcoder.jp\/contests\/` + rxCont + `$`,
`atcoder.jp\/contests\/` + rxCont + `\/tasks\/` + rxProb + `$`,

`^` + rxCont + `$`,
`^` + rxCont + `\s+` + rxProb + `$`,
}
)

str = strings.TrimSpace(util.StrClean(str))
if str == "" {
return Args{}, nil
}

for _, rgx := range valRx {
re := regexp.MustCompile(rgx)
if re.MatchString(str) {
// https://stackoverflow.com/a/46202939/9606036
match := re.FindStringSubmatch(str)
result := map[string]string{}
for i, name := range re.SubexpNames() {
if i != 0 && name != "" {
result[name] = match[i]
}
}

arg := Args{
Contest: result["cont"],
Problem: result["prob"],
}
return arg, nil
}
}
return Args{}, ErrInvalidSpecifier
}
139 changes: 139 additions & 0 deletions atcoder/atcoder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package atcoder

import (
"fmt"
"os"
"reflect"
"testing"

"github.com/go-rod/rod"
"github.com/joho/godotenv"
)

func login(usr, passwd string) (string, error) {
p, err := loadPage(fmt.Sprintf("%v/login", hostURL))
if err != nil {
return "", err
}
defer p.Close()

if _, err := p.Race().Element(`alert`).Handle(handleErrMsg).
Element(`footer.footer`).Do(); err != nil {
return "", err
}

// Check if current user is logged in.
if handle := p.MustEval(`userScreenName`).String(); handle != "" {
return handle, nil
}
// Otherwise, login.
p.MustElement("#username").Input(usr)
p.MustElement("#password").Input(passwd)
p.MustElement("#submit").MustClick().WaitInvisible()

if _, err := p.Race().ElementR(`.alert`, `Username or Password is incorrect`).
Handle(func(e *rod.Element) error { return fmt.Errorf(e.MustText()) }).
Element(`.navbar-right>li:last-child>a[class]`).Do(); err != nil {
return "", err
}

handle := p.MustEval(`userScreenName`).String()
return handle, nil
}

func getLoginCredentials() (string, string) {
// setup login access to use
usr := os.Getenv("ATCODER_USERNAME")
passwd := os.Getenv("ATCODER_PASSWORD")
return usr, passwd
}

func TestMain(m *testing.M) {
// Load local .env file.
godotenv.Load()

_, browserHeadless := os.LookupEnv("BROWSER_HEADLESS")
browserBin := os.Getenv("BROWSER_BINARY")
if err := Start(browserHeadless, "", browserBin); err != nil {
fmt.Println("Failed to start browser:", err)
os.Exit(1)
}

if _, err := login(getLoginCredentials()); err != nil {
fmt.Println("Login failed:", err)
Browser.Close()
os.Exit(1)
}

exitCode := m.Run()

Browser.Close()
os.Exit(exitCode)
}

func TestParse(t *testing.T) {
type args struct {
str string
}
tests := []struct {
name string
args args
want Args
wantErr bool
}{
{
name: "Test #1",
args: args{"https://atcoder.jp/contests/acl1"},
want: Args{"acl1", ""},
wantErr: false,
},
{
name: "Test #2",
args: args{"https://atcoder.jp/contests/m-solutions2020/tasks/m_solutions2020_a"},
want: Args{"m-solutions2020", "m_solutions2020_a"},
wantErr: false,
},
{
name: "Test #3",
args: args{"arc107"},
want: Args{"arc107", ""},
wantErr: false,
},
{
name: "Test #4", // Problem id need not match contest id.
args: args{"arc9999 aproblem"},
want: Args{"arc9999", "aproblem"},
wantErr: false,
},
{
name: "Test #5",
args: args{"in_valid"},
want: Args{},
wantErr: true,
},
{
name: "Test #6",
args: args{"in-valid in-valid"},
want: Args{},
wantErr: true,
},
{
name: "Test #7",
args: args{""},
want: Args{},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Parse(tt.args.str)
if (err != nil) != tt.wantErr {
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Parse() = %v, want %v", got, tt.want)
}
})
}
}
59 changes: 59 additions & 0 deletions atcoder/contests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package atcoder

import (
"regexp"
"time"
)

func (p *page) getCountdown() (time.Duration, error) {
// First check for virtual countdown.
if match := regexp.MustCompile(`var virtualStartTime = moment\("(.*)"\)`).
FindStringSubmatch(p.MustElement("html").MustHTML()); len(match) == 2 {
startTime, err := time.Parse(time.RFC3339Nano, match[1])
if err != nil {
return 0, err
}

dur := time.Until(startTime).Truncate(time.Second)
if dur < 0 { // Virtual already started; No countdown
dur = 0
}
return dur, nil
}

// Parse actual contest start time.
startTimeStr := p.MustEval("startTime._d.toISOString()").String()
startTime, err := time.Parse(time.RFC3339Nano, startTimeStr)
if err != nil {
return 0, err
}

dur := time.Until(startTime).Truncate(time.Second)
if dur < 0 { // Contest already started; No countdown
dur = 0
}
return dur, nil
}

// GetCountdown ...
func (arg Args) GetCountdown() (time.Duration, error) {
link, err := arg.VirtualPage()
if err != nil {
return 0, err
}

p, err := loadPage(link)
if err != nil {
return 0, err
}
defer p.Close()

_, err = p.Race().Element(`.alert`).Handle(handleErrMsg).
Element(`footer.footer`).Do()

if err != nil {
return 0, err
}

return p.getCountdown()
}
40 changes: 40 additions & 0 deletions atcoder/contests_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package atcoder

import (
"testing"
"time"
)

func TestArgs_GetCountdown(t *testing.T) {
tests := []struct {
name string
arg Args
want time.Duration
wantErr bool
}{
{
name: "Test #1",
arg: Args{"abc180", ""},
want: 0,
wantErr: false,
},
{
name: "Test #2",
arg: Args{"InVaLiD123", ""},
want: 0,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.arg.GetCountdown()
if (err != nil) != tt.wantErr {
t.Errorf("Args.GetCountdown() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Args.GetCountdown() = %v, want %v", got, tt.want)
}
})
}
}
Loading